dotnet / csharplang

The official repo for the design of the C# programming language
11.53k stars 1.03k forks source link

[Proposal] Contract relaxation #2588

Closed JustNrik closed 4 years ago

JustNrik commented 5 years ago

Basically, a type doesn't need to implement certain interface, implementing their members should be enough.

Example usage:

public class Person
{
    public void Work() {}
}

public interface IWorker
{
    void Work();
}

public class Business
{
    public List<IWorker> Workers { get; }
    public void Employ(IWorker worker)
        => Workers.Add(worker);
    public void Start()
        => Workers.ForEach(worker => worker.Work());
}

var business = new Business();
business.Add(new Person()); 
// even if Person doesn't implement IWorker, 
// it implements its members so the contract is fullfit and this should work
business.Start();

How would it work under the hood? same as ref structs with I(Async)Disposable

svick commented 5 years ago

This is quite similar to existing proposals https://github.com/dotnet/csharplang/issues/2548 and https://github.com/dotnet/csharplang/issues/2499.

How would it work under the hood? same as ref structs with I(Async)Disposable

I don't see how that's comparable. AFAIK, ref structs that have the Dispose method can be used in a using statement, even though they can't implement interfaces. But that's just syntax sugar, what you're proposing would require having an object that the runtime considers to implement the interface, which is very different.

JustNrik commented 5 years ago

I don't see how that's comparable. AFAIK, ref structs that have the Dispose method can be used in a using statement, even though they can't implement interfaces. But that's just syntax sugar, what you're proposing would require having an object that the runtime considers to implement the interface, which is very different.

I guess "impliclitly" implementing the interface at compile time would be enough, so even casting would actually work as long as the object fullly implements the interface.

For example

public class Person
{
    public void DoWork() {}
}

would get compiled into

public class Person : IWorker
{
    public void DoWork() {}
}

so it's about writing the analyzer for it and make it compile this way as long as the interface is fully implemented. This would also work for any other interface like IEquatable, IComparable, IEnumerable, etc...

svick commented 5 years ago

@JustNrik So it wouldn't work for types from other assemblies? What exactly are the rules for when the compiler decides to implement the interface?

Also, I believe doing it exactly this way would be a breaking change. Consider having something like:

public class Business
{
    public void Employ(Person worker) { }
}

public class DerivedBussiness : Business
{
    public void Employ(IWorker person) { }
}

If you have DerivedBussiness and invoke Employ with a Person, it will currently call Business.Employ(Person), but if this proposal was adopted, it would call DerivedBussiness.Employ(IWorker) instead.

john-h-k commented 5 years ago

Type A is declared in Assembly 1 Interface B is declared in Assembly 2

A has all the members to implement B

How does it work?

JustNrik commented 5 years ago

Thinking about it, I guess a wrapper type is the way to go. In that case,

public class Person
{
    public void Work();
}

would get translated into

public class Person
{
    public void Work();
}
public class <IWorker>__Person : IWorker
{
    private readonly Person _person;
    public (Person person) => _person = person;
    void IWorker.Work() => person.Work();
}

so types in other assemblies will benefit from this. And the compiler will only do this if there's not valid overload for your type, except for the interface so it won't be a breaking change for the DerivedBusiness example

HaloFour commented 5 years ago

The team has expressed a lot of interest in stuff like this and has put up a few very early proposals.

https://github.com/dotnet/csharplang/issues/1711 https://github.com/dotnet/csharplang/issues/164

I'd expect that anything that eventually shakes out will meet the goal of this request but will likely be much more flexible. For example, it seems that the team intentionally wants extension members to be able to participate.

svick commented 5 years ago

@JustNrik With that implementation, would casting work? E.g. (Person)(IWorker)new Person()? What about object identity: object.ReferenceEquals(person, (IWorker)person)?

JustNrik commented 5 years ago

@JustNrik With that implementation, would casting work? E.g. (Person)(IWorker)new Person()? What about object identity: object.ReferenceEquals(person, (IWorker)person)?

If Person doesn't implement explicitly IWorker but it has its members, then it will be new <IWorker>__Person(person) Casting to IWorker would be a narrowing conversion defined on __Person like this:

public static explicit operator IWorker(Person person)
    => new <IWorker>__Person(person);

so (IWorker)new Person() would get translated into new <IWorker>__Person(new Person()) now casting IWorker to Person would be another narrowing convertion

public static explicit operator Person(IWorker worker)
    => worker is <IWorker>__Person person ? person._person : throw new InvalidCastException();

ReferenceEquals... well it would compare <IWorker>__Person#_Person with the provided reference, I'm not sure how this one would work at all

YairHalberstadt commented 4 years ago

Closing as duplicate of (among others) #2548 and #2499.