Skip to content
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

Open
MadsTorgersen opened this issue Mar 25, 2022 · 71 comments
Open
Labels
Proposal Question Question to be discussed in LDM related to a proposal

Comments

@MadsTorgersen
Copy link
Contributor

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:

interface I
{
    static abstract string P { get; }
}
class C<T> where T : I
{
    void M() { Console.WriteLine(T.P); }
}
new C<I>().M(); // Error

If this were allowed, the call to C<I>().M() would try to execute I.P, which of course doesn't have an implementation! Instead the compiler prevents I (and interface) from being used as a type argument, because the constraint contains an interface (also I) that has static virtual members.

However, the situation can still occur in a way that goes undetected by the compiler:

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);
}
new D().M0(); // Exception

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 parameter T which is used as a constraint for U.

The runtime protects against this case by throwing an exception when M0 tries to instantiate M<> with I, but it would be better if the compiler could prevent it.

I propose that we change the rule so that:

An interface containing or inheriting static virtual members cannot be used as a type argument

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 parameter U is constrained by T 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.

@gerhard17
Copy link

gerhard17 commented Mar 28, 2022

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'.
New is that a combination of both (either/or) must be possible.
I think that a new language keyword is not needed for this case, because the information can be derived from the interface usage itself.

@333fred
Copy link
Member

333fred commented Mar 29, 2022

@333fred 333fred added the Proposal Question Question to be discussed in LDM related to a proposal label Mar 29, 2022
@333fred
Copy link
Member

333fred commented Mar 29, 2022

@jaredpar, would you please assign to whomever will be implementing this restriction?

@gerhard17
Copy link

I read these discussions and understand the problem.
But to my opinion it is an unsatisfactory solution bejond preview stage.

@wzchua
Copy link

wzchua commented Mar 29, 2022

From the example,

class D : C<I>
{
    public override void M<U>() => Console.WriteLine(U.P);
}

This would mean C<I> would be treated as an error?

@CyrusNajmabadi
Copy link
Member

This restricts the usefulness of such interfaces with static virtual members in a major way.

Can you give an example?

But to my opinion it is an unsatisfactory solution bejond preview stage.

Can you clarify why?

@gerhard17
Copy link

gerhard17 commented Mar 29, 2022

@CyrusNajmabadi

As an example: Code which implements the strategy pattern.
In this case a generic strategy, which works on different types implementing INumber<T>.

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);

When I understand the proposal right, then interface ICalculationStrategy<T> where T : INumber<T> will raise a compile time error.

This is the point I'm not satisfied with and I think it should be possible in a release version bejond preview.
Strategy is a common and very usefull pattern.

@CyrusNajmabadi
Copy link
Member

When I understand the proposal right, then interface ICalculationStrategy where T : INumber will raise a compile time error.

I don't believe that's the case. T is not an interface containing static abstracts there. So there would be no issue.

@gerhard17
Copy link

Maybe the proposal can clarify, where the runtime error is raised now and where the compiler error will be raised in future.

@CyrusNajmabadi
Copy link
Member

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 * to call them out):

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.

@gerhard17
Copy link

gerhard17 commented Mar 29, 2022

Thanks for your clarification.
No need to answer the runtime exception question. That's clear for me now.

I see no direct problem now.
But I will think about it... I'm fine with the proposal.

@CyrusNajmabadi
Copy link
Member

Terrific!

@AraHaan
Copy link
Member

AraHaan commented Jul 3, 2022

Because of this decision that breaks my codebase, I would like for
image
to be possible. But require implementing classes to implement those static members.

@MrJul
Copy link

MrJul commented Jul 19, 2022

Problem

I 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 List<T>, Task<T> or use LINQ at all. In my opinion, this is so constraining that I'd prefer to avoid abstract static members altogether.

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 INode, they act only on the instance interface members. They don't care at all about the static abstract members.

By closing the small hole in the wall, you've ended up trapped in the basement.

Solutions

  1. Accept the hole where it can't be detected and let the runtime throws, which isn't ideal but doesn't block simple usages.

  2. Add a new generic constraint that forces a generic parameter to be of a type having all static members implemented whenever you want to use static abstract members. I'll use concrete T in the following examples. (Note that this can only be done before static abstract members are officially released since it's a breaking change.)

In the INode example, CreateNode has to change to use static abstract members:
T CreateNode<T>(string text) where T : concrete INode => T.Create(text);

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();

@Joe4evr
Copy link
Contributor

Joe4evr commented Jul 19, 2022

2. Add a new generic constraint that forces a generic parameter to be of a type having all static members implemented whenever you want to use static abstract members. I'll use concrete T in the following examples.

I've said it before and I'll say it again: It'd be quite useful to bring a concrete constraint in general, not only for interfaces with static abstract members.

@fitdev
Copy link

fitdev commented Jul 26, 2022

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 static virtual members are allowed as type constraints (so long as they provide DIMs for all/any static abstract members they might have inherited).

@EamonNerbonne
Copy link
Contributor

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 TFooBar : IFooBar<TFooBar, TFooBarRules>, then it's sometimes necessary to type method arguments that are conceptually TFooBar as IFooBar<TFooBar, TFooBarRules> and cast the objects - because passing them TFooBar means that TFooBarRules cannot be inferred. And that in turn makes API's unusable.

@Joe4evr
Copy link
Contributor

Joe4evr commented Aug 15, 2022

type inference will not infer type arguments of an interface, so if I have a TFooBar : IFooBar<TFooBar, TFooBarRules>, then it's sometimes necessary to type method arguments that are conceptually TFooBar as IFooBar<TFooBar, TFooBarRules> and cast the objects - because passing them TFooBar means that TFooBarRules cannot be inferred.

#5556

@EamonNerbonne
Copy link
Contributor

EamonNerbonne commented Aug 16, 2022

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 TSelf here is so that type inference can work (and yes, that would be a perf pitfall, but that's not always relevant).

The issues with this solution to the hole therefore doesn't just limit where static abstract is potentially useful, it also imposes limits on some usages of the motivating example of INumber related types.

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 concrete constraint (or something similar) rather than impose this limitation everywhere. But even a runtime crash would be preferable to me (though I understand the distaste for runtime exceptions).

@AraHaan
Copy link
Member

AraHaan commented Aug 16, 2022

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.

@dcuccia
Copy link

dcuccia commented Aug 26, 2022

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....

@333fred
Copy link
Member

333fred commented Aug 26, 2022

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.

@dcuccia
Copy link

dcuccia commented Aug 26, 2022

@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 IService into IService and IStaticService, but then I'd argue we're "enabling static abstract interfaces" not "enabling static abstract in interfaces."

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

@CyrusNajmabadi
Copy link
Member

builder.Services.AddServicesForAssemblyContaining<IService>(builder.Configuration);

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 public static abstract void AddServiceDependencies. So any attempt to call that would certainly blow up.

@dcuccia
Copy link

dcuccia commented Aug 26, 2022

IService legit does not have an impl for public static abstract void AddServiceDependencies. So any attempt to call that would certainly blow up.

ICustomerService implements IService, so all three concrete implementations are required to implement it - I provided an example in CosmosDbCustomerService

@CyrusNajmabadi
Copy link
Member

@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.

@calvin-charles
Copy link

As I said, I can understand where you're coming from with this. It's certainly possible we decide to accept this type hole in the future and loosen the restriction. As often do, we've started more narrow, and we're seeing where people run into limitations (as you are right now :) ).

Why not disallow calling any virtual/abstract members of an interface and allow it as a type argument?

@HazyFish
Copy link

HazyFish commented Dec 26, 2023

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.

@elTRexx
Copy link

elTRexx commented Mar 23, 2024

Hi. I stumble around this issue. My main goal is to provide a static Create property (or a method whatever), defined from my interface.
It should be static since it has nothing to deal with instance object (as it will create one actually).
It should be abstract, and not virtual, in order to "force" implementing classes to provide such property/method.

But then I fall into CS8920 error pit.
So is there a way to have a virtual static interface member to be always implemented in future implementing classes,
or a mechanism (I like the concrete generic Type constrain proposal) to avoid CS8920 error ?

Thank you.

@wrgarner
Copy link

wrgarner commented May 31, 2024

Am I crazy or isn't Task<T> likely to be the most common CS8920? It certainly would be in my codebase. I worry that anytime I use a static abstract, I'll just be asking some future developer to rewrite it as soon as my type needs to be returned from an async method.
(Let's face it, that future developer is me if I'm lucky). At this point I'm guessing the common workaround will be converting it to virtual and throwing an exception. I don't hate that as much as some people would, but it's dissatisfying for sure.

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.

@aradalvand
Copy link
Contributor

aradalvand commented Oct 9, 2024

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 List<> of that thing, and can't return it from an async method, can't have in a Func or pass it in an Action, can't get an instance of it from a service provider. Even though List/Task/Func/Action, etc. do not give a damn about static members of T, yet we still have to pay the price because the compiler authors wanted to prevent a runtime exception in the most niche scenario imaginable.

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.

@KatDevsGames
Copy link

KatDevsGames commented Oct 9, 2024

if and when we find specific common situations that are overly constrained by it.

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?

@CyrusNajmabadi
Copy link
Member

So what happens next?

Someone finds and proposes a viable solution.

@dcuccia
Copy link

dcuccia commented Oct 9, 2024

TreatErrorAsWarningProperty

@KatDevsGames
Copy link

KatDevsGames commented Oct 9, 2024

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.

@CyrusNajmabadi
Copy link
Member

Nobody has a problem with the runtime exception in the case you describe

I would have a problem with that. I don't want this case randomly blowing up at runtime.

@KatDevsGames
Copy link

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.

@CyrusNajmabadi
Copy link
Member

It doesn't seem contrived to me. I understand that you feel differently.

@BenjaBobs
Copy link

Nobody has a problem with the runtime exception in the case you describe

I would have a problem with that. I don't want this case randomly blowing up at runtime.

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.

@CyrusNajmabadi
Copy link
Member

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 :)

@KatDevsGames
Copy link

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".

@HaloFour
Copy link
Contributor

@KatDevsGames

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.

@KatDevsGames
Copy link

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.

@aradalvand
Copy link
Contributor

aradalvand commented Oct 10, 2024

@CyrusNajmabadi How about introducing the concrete constraint as already described here? That sounds like a proper language-level solution.

@aradalvand
Copy link
Contributor

aradalvand commented Oct 10, 2024

Cyrus isn't the only member of the language design team to find the potential of a runtime exception to be an unacceptable risk

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.

@CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi How about introducing the concrete constraint as already described here? That sounds like a proper language-level solution.

I would be willing to review such a proposal if someone wants to create it.

@CyrusNajmabadi
Copy link
Member

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.

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.

@aradalvand
Copy link
Contributor

aradalvand commented Oct 10, 2024

You're definitely welcome to have that opinion. We don't agree.

@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 someone wants to get data indicating this is more important

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.

has a reasonable proposal for a way to resolve this

People have pointed out concrete quite a number of times including here and here; I think the work of detailing an elaborate proposal (if there's need for that) is better done by people like yourself who are better equipped for it both in terms of experience, and also time, since we're not the ones getting paid to work on C#.

@EamonNerbonne
Copy link
Contributor

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.

@CyrusNajmabadi
Copy link
Member

but you haven't provided any evidence for your claim whatsoever

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.

and also time

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.

@CyrusNajmabadi
Copy link
Member

Still, the workaround today of using a static virtual that throws at least exists.

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?

@EamonNerbonne
Copy link
Contributor

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.

@CyrusNajmabadi
Copy link
Member

Error reporting

You can make this an analyzer error if that's a concern of yours.

Failing to implement a static abstract is a compile-time error

You can make that an analyzer error if that's a concern.

The same type hole as previously still exists

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Proposal Question Question to be discussed in LDM related to a proposal
Projects
None yet
Development

No branches or pull requests