-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Disallow interfaces with static virtual members as type arguments #5955
Comments
I understand that the language should provide language constructs which are executable at runtime and do not throw unneccessary exceptions. But I do not agree that constraints must not contain interfaces with static virtual members. This restricts the usefulness of such interfaces with static virtual members in a major way. Instead I recommend that the the language defines, that such constraints can only be fulfilled by non interface types like structs or classes (implementing this static members) . This should be checked by the compiler. Similar constraints exist today: 'where T : struct, I' and 'where T : class, I'. |
This was discussed and accepted in LDM: https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts |
@jaredpar, would you please assign to whomever will be implementing this restriction? |
I read these discussions and understand the problem. |
From the example, class D : C<I>
{
public override void M<U>() => Console.WriteLine(U.P);
} This would mean |
Can you give an example?
Can you clarify why? |
As an example: Code which implements the strategy pattern.
When I understand the proposal right, then This is the point I'm not satisfied with and I think it should be possible in a release version bejond preview. |
I don't believe that's the case. T is not an interface containing static abstracts there. So there would be no issue. |
Maybe the proposal can clarify, where the runtime error is raised now and where the compiler error will be raised in future. |
The compiler error would be given at a site where an interface is used as a type argument and that interface contains at least one static without an impl (e.g. a static-abstract). In your case, here are the places where you supply type arguments (i've used interface INumber<T> {
//static abstract members here
}
interface ICalculationStrategy<T> where T : INumber<*T*> {
T PerformCalculation(T arg1, T arg2);
}
class ConcreteCalculationStrategy<T> : ICalculationStrategy<*T*> where T : INumber<*T*> {
public T PerformCalculation(T arg1, T arg2) {
// use static members of T for some math
}
}
// some concrete usage
ICalculationStrategy<*double*> strategy = new ConcreteCalculationStrategy<*double*>();
double result = strategy.PerformCalculation(1.0, 2.0); In none of those places are you passing an interface with abstract-statics. So this is all fine. -- I'm not sure on the runtime side yet. I'll have the runtime people weigh in on that. |
Thanks for your clarification. I see no direct problem now. |
Terrific! |
ProblemI understand the hole that this closes, but this seems very, very limiting. This change puts interfaces with static abstract members in a whole new category where you can't use them in most places you'd expect to, similar to ref structs. Except ref structs are usually used in specific, limited contexts. Interfaces are everywhere. Adding a new abstract static member to an interface should be a breaking change to implementors only, not to every single usage as a type parameter. You now can't use the interface type in common types such as Let's take the following interface: interface INode
{
string Text { get; }
static abstract INode Create(string text);
} The abstract static method is only for convenience, so you can do things like: T CreateNode<T>(string text) where T : INode => T.Create(text); which is awesome, and what abstract static methods were made for. Until you realize that all the following don't work anymore: var nodes = new List<INode>(); // CS8920
void Process(IEnumerable<INode> nodes) { } // CS8920
string[] GetTexts(INode[] nodes) => nodes.Select(node => node.Text).ToArray(); // CS8920
Task<INode> GetNodeAsync() { ... } // CS8920 Those methods aren't made to accept concrete implementations of By closing the small hole in the wall, you've ended up trapped in the basement. Solutions
In the The examples in the OP become: interface I
{
static abstract string P { get; }
}
class C<T> where T : concrete I
{
void M() { Console.WriteLine(T.P); }
}
new C<I>().M(); // CS8920 abstract class C<T>
{
public abstract void M<U>() where U : T;
public void M0() { M<T>(); }
}
interface I
{
static abstract string P { get; }
}
class D : C<I>
{
public override void M<U>() => Console.WriteLine(U.P); // CS8920
}
new D().M0(); |
I've said it before and I'll say it again: It'd be quite useful to bring a |
Just to make sure I understand the file decision made by the team here: shouldn't this issue be called "Disallow interfaces with static abstract members as type arguments" instead of "Disallow interfaces with static virtual members as type arguments"? Because as of now in DotNet 7 Preview 6, VS 2022 17.3 Preview 3 interfaces with |
This breaks a CRTP-style pattern I was using to pass along objects with many varied shapes (i.e. interface implementations) that conform to certain patterns (i.e. must provide certain static abstract members). In particular, there's an interaction with type-inference limits that's painful: I type inference will not infer type arguments of an interface, so if I have a |
|
This is still quite inconvenient, even simply for using the new INumber related interfaces, let alone trying to use this more broadly. The issue MrJul describes above applies even to examples using INumber; e.g.: using System.Numerics;
Console.WriteLine(Bla(new[] { 3, }.AsEnumerable()));
Console.WriteLine(Bla2(new[] { 3, }.AsEnumerable()));
Console.WriteLine(Bla3(new[] { new Num(3), }.AsEnumerable())); //only this actually compiles without error
Console.WriteLine(Bla4(new[] { new Num(4), }.AsEnumerable()));
static TResult Bla<TSelf, TResult>(IEnumerable<IMultiplicativeIdentity<TSelf, TResult>> nums)
where TSelf : IMultiplicativeIdentity<TSelf, TResult>
=> TSelf.MultiplicativeIdentity;
static TResult Bla2<TSelf, TResult>(IEnumerable<TSelf> nums)
where TSelf : IMultiplicativeIdentity<TSelf, TResult>
=> TSelf.MultiplicativeIdentity;
static TResult? Bla3<TSelf, TResult>(IEnumerable<IThing<TSelf, TResult>> nums)
where TSelf : IThing<TSelf, TResult>
=> default(TResult);
static TResult? Bla4<TSelf, TResult>(IEnumerable<TSelf> nums)
where TSelf : IThing<TSelf, TResult>
=> default(TResult);
interface IThing<TSelf, TResult> where TSelf : IThing<TSelf, TResult> { }
sealed record Num(int Value) : IThing<Num, int>; The reason to use interface-typed parameters rather than The issues with this solution to the hole therefore doesn't just limit where Given the fairly convoluted scenario this protects against, and the fact that even without this additional limitation (disallowing interfaces with static virtual members as type arguments) the error is at least caught at runtime - is this really worth it? This features makes understanding static abstract more complex in normal usage, and imposes inconvenient limitations; the upside is compile-time rather than runtime error-reporting in hopefully niche cases. I also support the idea MrJul proposed to add an additional |
Since this is not possible now with compile errors, how about a way to tell the C# compiler that an explicit static implementation must be overridden instead using an attribute to flag it as "Required static members that implements the 'X' interface has not been implemented in "Y'." error. |
I'm not sure if this is the right place for this feedback, but after the third time this week that I've stumbled upon this limitation with Preview 7, I thought I'd share a couple of bog-standard use-cases: // Bootstrapping...
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ILocator, GoodLocator>(); // causes compiler error
// Mocking...
var sub = NSubstitute.Substitute.For<ILocator>(); // causes compiler error
// Factories...
builder.Services.AddSingleton<ILocator>(services => LocatorFactory.GetLocator(true)); // causes compiler error
public interface ILocator
{
public static abstract string ConnectionString { get; }
}
public class GoodLocator : ILocator { public static string ConnectionString => "C:\\Data\\my.db"; }
public class BetterLocator : ILocator { public static string ConnectionString => "https:\\my.db"; }
public interface ILocatorFactory
{
public static abstract ILocator GetLocator(bool onlyTheBest);
}
public class LocatorFactory : ILocatorFactory
{
public static ILocator GetLocator(bool onlyTheBest) => onlyTheBest switch
{
true => new BetterLocator(),
false => new GoodLocator()
};
} All three cases throw this compiler error. The beauty of this static abstract feature is not just in greenfield math libs, but in our ability to remove cruft in existing code, where static methods were the answer all along. @MadsTorgersen explicitly mentions factories as a great use case in his recent NDC Copenhagen talk, for example. Am I understanding/utilizing this feature as intended? Is this a Preview 7 bug, or do I have it all wrong? Please inform! :) P.S. I'm only showing static methods for the implementations here, but let's assume there's some great brownfield instance-based members/methods on these classes...we're just sprinkling on a little more static abstract awesomeness.... |
Can you clarify exactly how you'd use the static abstract method there @dcuccia? Given that you're using a DI pattern, you're not operating on generic type parameters, so I don't know how you'd actually call that method. |
@333fred sure. I assumed above that there were instance methods/properties as well, I was just highlighting the incremental static additions I'd add. Swag below at a more real-world scenario. The work-around alternative is probably to segregate the using System.Reflection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using FluentAssertions;
using NSubstitute;
using Xunit;
public interface IStaticService
{
// cross-cutting concern: have services boostrap their own dependencies (opinionated approach...)
public static abstract void AddServiceDependencies(IServiceCollection services, IConfiguration configuration);
}
// this version of IService causes build error
// decorating concrete classes with IStaticService instead avoids the build error, but does not enforce
// the contract at the IService level
// public interface IService : IStaticService { string ServiceName { get; } }
public interface IService { string ServiceName { get; } }
public record Customer(string Id, string Name);
public interface ICustomerService : IService
{
List<Customer> GetAllCustomers();
}
public interface IRepository<T>
{
List<T> Find(Predicate<T> query);
}
public class CosmosDbRepository : IRepository<Customer>
{
public List<Customer> Find(Predicate<Customer> query) => throw new NotImplementedException();
}
public class CosmosDbCustomerService : ICustomerService, IStaticService
{
private readonly IRepository<Customer> _repository;
public CosmosDbCustomerService(IRepository<Customer> repository)
{
_repository = repository;
}
public string ServiceName => nameof(CosmosDbCustomerService);
public List<Customer> GetAllCustomers() => _repository.Find(c => c.Name != null);
public static void AddServiceDependencies(IServiceCollection services, IConfiguration configuration)
{
services.AddOptions();
services.TryAddSingleton<CosmosDbRepository>(); // boostraps the IRepository<T> container
services.TryAddSingleton<IRepository<Customer>, CosmosDbRepository>(); // boostraps the IRepository<T> container
}
}
public class RavenDbCustomerService : ICustomerService, IStaticService // IStaticService added here to avoid compilation error
{
public string ServiceName => nameof(RavenDbCustomerService);
public static void AddServiceDependencies(IServiceCollection services, IConfiguration configuration) { }
public List<Customer> GetAllCustomers() => new();
}
public enum CustomerServiceType
{
CosmosDb,
RavenDb
}
public static class CustomerServiceFactory
{
// simplify factories that don't themselves need instance members by using static contracts
public static ICustomerService GetCustomerService(IServiceProvider provider, CustomerServiceType type) => type switch
{
CustomerServiceType.CosmosDb => provider.GetRequiredService<CosmosDbCustomerService>(),
CustomerServiceType.RavenDb => provider.GetRequiredService<RavenDbCustomerService>(),
_ => throw new ArgumentOutOfRangeException()
};
}
public class BusinessApi
{
private readonly ICustomerService _customerService;
public BusinessApi(ICustomerService customerService) =>
_customerService = customerService;
public List<string> GetPrintableCustomerList() =>
_customerService
.GetAllCustomers()
.Select(c => $"{c.Id}: {c.Name}")
.ToList();
}
public static class BusinessApiServiceExtensions
{
public static void AddServicesForAssembly(this IServiceCollection services, IConfiguration configuration)
{
Type interfaceType = typeof(IService);
IEnumerable<TypeInfo> serviceTypesInAssembly = interfaceType.Assembly.DefinedTypes
.Where(x => !x.IsAbstract && !x.IsInterface && interfaceType.IsAssignableFrom(x));
foreach (var serviceType in serviceTypesInAssembly)
{
// add the service itself
services.TryAdd(new ServiceDescriptor(serviceType, serviceType, ServiceLifetime.Singleton)); // runtime error, I think
// add any required service dependencies, besides the service (e.g. what's needed for construction)
serviceType.GetMethod(nameof(IStaticService.AddServiceDependencies))! // BOOTSTRAPPING EXAMPLE
.Invoke(null, new object[] {services, configuration});
}
}
}
public class BusinessApiTests
{
[Fact]
public static void GetPrintableCustomerList_ReturnsEmptyList_WhenNoCustomersExist()
{
// Arrange
// compiler error CS8920 "static member does not have a most specific implementation of the interface"
ICustomerService subCustomerService = NSubstitute.Substitute.For<ICustomerService>();
subCustomerService.GetAllCustomers().Returns(new List<Customer>()); // MOCK EXAMPLE
BusinessApi api = new (subCustomerService);
// Act
List<string> shouldBeEmpty = api.GetPrintableCustomerList();
// Assert
shouldBeEmpty.Count.Should().Be(0);
}
[Fact]
public static void WebApplication_Build_ShouldNotThrowException()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Services.AddServicesForAssembly(builder.Configuration);
// compiler error CS8920 "static member does not have a most specific implementation of the interface"
builder.Services.AddSingleton<ICustomerService>(services =>
CustomerServiceFactory.GetCustomerService(services, CustomerServiceType.CosmosDb));
// Act
WebApplication app = builder.Build();
ICustomerService customerService = app.Services.GetRequiredService<ICustomerService>();
// Assert
customerService.Should().NotBeNull();
}
} EDIT - I should mention the BusinessApiServiceExtensions boostrapping approach was borrowed from a YouTube presentation by Nick Chapsas. EDIT 2 - Simplified the AddServicesForAssembly extension and replaced erroneous ILocator reference with ICustomerService EDIT 3 - Updates to streamline the point, and make compilable (with dependencies). See here for full solution: https://github.com/dcuccia/StaticAbstractDemo |
It's genuinely unclear to me what would be expected to happen with this. You're providing IService as the type-arg, but IService legit does not have an impl for |
|
@KatDevsGames Your post was hidden for violations of the .Net code of conduct: https://dotnetfoundation.org/about/policies/code-of-conduct Please refrain from similar posts. |
Why not disallow calling any virtual/abstract members of an interface and allow it as a type argument? |
I have a very simple use case of serializing an object of interface type to JSON. The interface happens to have a static abstract method that has nothing to do with the JSON serialization. However, this line of code currently cannot compile due to CS8920 in C#. JsonSerializer.Serialize(new Model { ... } as IModel); I think we should disallow calling any virtual/abstract members of an interface and allow it as a type argument like @calvin-charles suggested. |
Hi. I stumble around this issue. My main goal is to provide a static Create property (or a method whatever), defined from my interface. But then I fall into CS8920 error pit. Thank you. |
Am I crazy or isn't I suppose the real intended use case is still in heavy numeric code, and I guess most of that isn't async. I realize that the language team has a difficult problem here and I think they overall do a great job with these kinds of tradeoffs. And I certainly won't claim to have a novel solution. But anyway here's my hope that some more time gets put into trying. |
This is the most ridiculous, nonsensical compiler restriction I have ever seen in my life. Added an abstract static member to an interface, now suddenly can't have a This simple restriction has an egregious ripple effect that makes numerous completely unrelated use cases impossible. The trade-off was patently clear. The decision was absurd. |
Situations such as the 30 or so use cases that have been posted here over the last two and a half years, you mean? "If" has already happened and "when" was long before now. So what happens next? |
Someone finds and proposes a viable solution. |
|
There were multiple viable solutions already proposed in this thread including just emitting a runtime exception in the deranged corner cases you seem to be so concerned over and letting people get on with things in the overwhelming majority of situations where the restriction makes no sense whatsoever. Nobody has a problem with the runtime exception in the case you describe. It's the compiler restriction on code that doesn't even touch the static abstract fields that is utterly absurd. It has nothing to do with actually using the static virtual method and everything to do with just being able to return an object from a Task<T>. The Task & List restrictions alone are enough to make the entire static virtual feature nearly useless. You may as well just remove it from the language if that's going to be the case. |
I would have a problem with that. I don't want this case randomly blowing up at runtime. |
Except it's not random, is it? It's an utterly contrived corner case that doesn't reflect the overwhelming usage. This kind of whattaboutism is what destroys forward progress. It's the same attitude as banning bricks because they get thrown through shop windows 0.1% of the time. |
It doesn't seem contrived to me. I understand that you feel differently. |
What are then your thoughts on the following: public interface IFoo
{
static abstract void Bar();
}
public class Baz
{
public static void Trigger<T>(T inst)
where T : IFoo
{
T.Bar();
}
}
// System.ArgumentException wrapping a System.Security.VerificationException
typeof(Baz).GetMethod("Trigger").MakeGenericMethod(typeof(IFoo));
// Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
IFoo myFoo = null;
Baz.Trigger((dynamic)myFoo); While not exactly specific to the static virtual members, they are runtime errors that can already occur when using this feature. |
I'm ok with dynamic causing runtime errors. 'dynamic' is the feature that says 'resolve at runtime. I'm not ok with it here for non-dynamic cases. Thanks :) |
The overwhelming majority of developers ARE fine with it but hold the wire, everyone else's dozens of legitimate use cases be damned because Cyrus Najmabadi is "not ok with it". |
Do you find that method of argument to be at all effective? Cyrus isn't the only member of the language design team to find the potential of a runtime exception to be an unacceptable risk, and short of convincing them of a path forward that is safe enough to be implemented you're not going to find something happening here. It's not going to happen by browbeating the members of the language team. |
It's clearly not going to happen at all regardless of what is said. That was made very clear by the two and a half years of comment history in this thread being met with nothing but stubborn refusal. Unsubscribing from this thread since it's very clear that the language team has already made up its mind and isn't interested in actually receiving input. |
@CyrusNajmabadi How about introducing the |
I would argue strenuously that rendering interface abstract static members practically unusable in so many valid use cases was absolutely worse than allowing a runtime exception (and perhaps documenting it) in a scenario that less than 0.00001% of people are likely to ever come across. But that's just me. |
I would be willing to review such a proposal if someone wants to create it. |
You're definitely welcome to have that opinion. We don't agree. If someone wants to get data indicating this is more important, and has a reasonable proposal for a way to resolve this(with whatever might be needed in lang or runtime), we'd definitely look at the ideas. |
@CyrusNajmabadi You're definitely welcome not to agree, but you haven't provided any evidence for your claim whatsoever, whereas the actual feedback on this issue so far (such as the ratio of thumbs-ups/thumbs-downs, plus the actual written feedback by users) is all evidence in support of my stance.
If you aren't already convinced that this is "important", I honestly don't know what to say. I believe the case has been made patently clear.
People have pointed out |
The argument for avoiding runtime exceptions is reasonable, but it sounds strained to me. The language is full of other places where typesafety is left to runtime checks; better to be able to express something that needs checking at runtime than not to be able to express it at all. Really major features such as nullable references types, static initialization ordering, reflection, array access - all are crucial to virtually every program, and yet we accept that soundness cannot be determined compile time. If anything, this is a much easier runtime check to document and understand than issues with static initialization ordering, for instance. Still, the workaround today of using a static virtual that throws at least exists. However, a concrete constraint sounds like it would be a great compromise that doesn't involve everybody not forgetting to override a static virtual or just give up on static abstract to express type-classes entirely. |
I'm not the one wanting a change here. I'm fine with the status quo. If you want a change you'll have to convince people on the team it is needed.
My time is entirely full currently with tons of other things we think are much more important. I can help with reviews, but that's about it. Sorry. |
Yup. Seems very workable to me. And you now aren't blocked and you get a runtime exception which you seem ok with. I'm not really understanding why this isn't sufficient. What doesn't work in such a situation? |
Error reporting and dev experience is worse. Failing to implement a static abstract is a compile-time error; failing to override a static virtual is perfectly fine. The same type hole as previously still exists, except now error reporting never catches it, instead of catching it in all straightforward cases. static virtual won't even report an error in the direct, non-type-constrained case. |
You can make this an analyzer error if that's a concern of yours.
You can make that an analyzer error if that's a concern.
Yes. but you can at least compile and run. And if you do need compile time errors enough on the need for override, it's a trivial analyzer to add. |
This proposal attempts to address a type hole that has been pointed out a number of times with static virtual members, e.g. here.
Static virtual members (Proposal: static-abstracts-in-interfaces.md, tracking issue: #4436) have a restriction disallowing interfaces as type arguments where the constraint contains an interface which has static virtual members.
This is to prevent situations like this:
If this were allowed, the call to
C<I>().M()
would try to executeI.P
, which of course doesn't have an implementation! Instead the compiler preventsI
(and interface) from being used as a type argument, because the constraint contains an interface (alsoI
) that has static virtual members.However, the situation can still occur in a way that goes undetected by the compiler:
Here
I
is not directly used as a constraint, so the compiler does not detect a violation. But it is still indirectly used as a constraint, when substituted for the type parameterT
which is used as a constraint forU
.The runtime protects against this case by throwing an exception when
M0
tries to instantiateM<>
withI
, but it would be better if the compiler could prevent it.I propose that we change the rule so that:
This simple rule protects the assumption that any type used as a type argument satisfies itself as a constraint. That is the assumption behind the language allowing e.g. the
M<T>
instantiation above, where the type parameterU
is constrained byT
itself.It is possible that this restriction would limit some useful scenarios, but it might be better to be restrictive now and then find mitigations in the future if and when we find specific common situations that are overly constrained by it.
The text was updated successfully, but these errors were encountered: