Good reason to inherit from generics in C #

I know this topic has been discussed many times: What are the good reasons why .NET developers might inherit from one of the typical parameter types?

But now I think it's really necessary ...

I will try to explain with a simple example why we need inheritance from generic types.

In short, the motivation is to make it much easier to develop what I call Compile Time Order Enforcement, which is very popular in ORM Framework.

Let's assume we are building a database.

Here is an example of building a transaction with such a card:

public ITransaction BuildTransaction(ITransactionBuilder builder)
{
    /* Prepare the transaction which will update specific columns in 2 rows of table1, and one row in table2 */
    ITransaction transaction = builder
        .UpdateTable("table1") 
            .Row(12)
            .Column("c1", 128)
            .Column("c2", 256)
            .Row(45)
            .Column("c2", 512) 
        .UpdateTable("table2")
            .Row(33)
            .Column("c3", "String")
        .GetTransaction();

    return transaction;
}

      

Since each line returns some interface, we would like to return them in such a way that the developer cannot commit an error in sequential order, and the actual usage will be applied at compile time, which will also simplify the implementation and use of TransactionBuilder, because the developer simply cannot be wrong , eg:

    { 
        ITransaction transaction = builder
            .UpdateTable("table1") 
            .UpdateTable("table2")  /*INVALID ORDER: Table can't come after Table, because at least one Row should be set for previous Table */
    }
    // OR
    {
        ITransaction transaction = builder
            .UpdateTable("table1") 
                .Row(12)
                .Row(45) /* INVALID ORDER: Row can't come after Row, because at least one column should be set for previous row */
    }

      

Now let's take a look at the ITransactionBuilder interface today, WITHOUT inheriting from generic, which will provide the required ordering at compile time:

    interface ITransactionBuilder
    {
        IRowBuilder UpdateTable(string tableName);
    }
    interface IRowBuilder
    {
        IFirstColumnBuilder Row(long rowid);
    }
    interface IFirstColumnBuilder
    {
        INextColumnBuilder Column(string columnName, Object columnValue);
    }
    interface INextColumnBuilder : ITransactionBuilder, IRowBuilder, ITransactionHolder
    {
        INextColumnBuilder Column(string columnName, Object columnValue);
    }
    interface ITransactionHolder
    {
        ITransaction GetTransaction();
    }
    interface ITransaction
    {
        void Execute();
    }

      

As you can see, we have 2 interfaces for the column builder "IFirstColumnBuilder" and "INextColumnBuilder" which are not really required, and remember that this is a very simple example of a compile-time state machine, and in a more complex problem, a number of Optional interfaces will grow dramatically.

Now suppose we can inherit from generics and prepare interfaces like this

interface Join<T1, T2> : T1, T2 {}
interface Join<T1, T2, T3> : T1, T2, T3 {}
interface Join<T1, T2, T3, T4> : T1, T2, T3, T4 {} //we use only this one in example

      

We can then rewrite our interfaces in a more intuitive style and with a single column constructor and without affecting the order

interface ITransactionBuilder
{
    IRowBuilder UpdateTable(string tableName);
}
interface IRowBuilder
{
    IColumnBuilder Row(long rowid);
}
interface IColumnBuilder
{
    Join<IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder> Column(string columnName, Object columnValue);
}
interface ITransactionHolder
{
    ITransaction GetTransaction();
}
interface ITransaction
{
    void Execute();
}

      

So we used Join <...> to combine existing interfaces (or "next steps"), which is very useful for developing state machines.

Of course, this particular problem could be solved by adding a capability in C # to "connect" interfaces, but it is clear that if one could inherit from generics, the problem would not exist at all, and it is also clear that compilation time ordering is very useful.

BTW. For syntax like

    interface IInterface<T> : T {}

      

There are no "what if" cases other than the inheritance loop that can be found at compile time.

I think that AT LEAST for interfaces needs this function 100%

Hello

+3


source to share


3 answers


You grossly underestimate the complexity of this feature. Something is wrong as if I am writing

public class Fooer
{
    public void Foo();
}

public class Generic<T> : T
{
    public void Foo();
}

var genericFooer = new Generic<Fooer>();

      

Now I have a problem with conflicting member signatures.

The large number of interfaces to support the free api as you describe represents a small subset of the use cases and, as I mentioned in my comment, is relatively easy to support by metaprogramming. I'm not sure if anyone has created a T4 template to translate DSL into interface declarations, but it is certainly doable. I don't think interfaces have ever been created with every combination of transitions represented in the final destination machine. Of course, this is not worth the change, which I am sure would be extremely difficult to implement and have all sorts of odd cases like this.

Update . Another thing to keep in mind is that it has a very large potential for breaking existing code that uses Reflection. This will include things like Entity Framework, IoC containers, Automapper, maybe more. I would be surprised if there were no new use cases introduced in any of these examples, introduced by such a change that would cause errors or unexpected behavior.



Of course, this is true to some extent for any language change, but this one significant one is likely to have a big impact. Again, this is a large cost that must be balanced against the relatively small benefit.

Update 2 . If we restrict it to only interfaces, we still have this problem:

public interface IFooer
{
    public void Foo();
}

public interface IGeneric<T> : T
{
    public int Foo();
}

      

They are incompatible because you cannot have two methods that differ only in return type, even on an interface.

+2


source


I looked through the comments and here are my findings (combined with my opinion).

  • It is not as easy as I thought to allow inheritance from generics, even for interfaces.
  • A solution for the Fluent API is a must (maybe not through generics inheritance, but through a new syntax for "interconnecting interfaces", which is proposed later), since it is very easy to use and allows you to refuse many developer errors during compilation time - popularity will only be grow.

I believe 1 and 2 are more than possible, but for the time being I decided to print my best solution to the problem I described

So I changed interfaces to this ("new syntax" commented)

    public interface ITransactionBuilder
    {
        IRowBuilder UpdateTable(string tableName);
    }
    public interface IRowBuilder
    {
        IColumnBuilder Row(long rowid);
    }

    public interface INextColumnBuilder : IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder { }
    public interface IColumnBuilder
    {
        INextColumnBuilder Column(string columnName, Object columnValue);
        //I still want to write something like this here!!! But currently we may only to comment it :(
        //<IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder> Column(string columnName, Object columnValue)
    }
    public interface ITransactionHolder
    {
        ITransaction GetTransaction();
    }
    public interface ITransaction
    {
        //Execute func declaration
    }

      

So, the overhead is not that big. Compared to being "mergeable" we only need to declare dummy interfaces for the return types and then inherit from the developer, so let's take a look at TransactionBuilder (the developer) ...



    class TransactionBuilder : INextColumnBuilder
    //I want to write this: class TransactionBuilder : IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder
    //But then we will fail on "return this;" from Column() func, because class TransactionBuilder not inherits from INextColumnBuilder, and again interface joining should solve it...
    {
        public virtual IRowBuilder UpdateTable(string tableName)
        {
            m_currentTable = new TableHolder(tableName);
            m_commands.Add(new UpdateCommand(m_currentTable));
            return this;
        }
        public virtual IColumnBuilder Row(long rowid)
        {
            m_currentRow = new RowHolder(rowid);
            m_currentTable.AddRow(m_currentRow);
            return this;
        }
        public virtual INextColumnBuilder Column(string columnName, Object columnValue)
        //And the dream is: <IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder> Column(string columnName, Object columnValue)
        {
            m_currentRow.AddColumn(columnName, columnValue);
            return this;
        }
        public virtual ITransaction GetTransaction()
        {
            return new Transaction(m_commands);
        }

        private ICollection<ICommand> m_commands = new LinkedList<ICommand>();
        private TableHolder m_currentTable;
        private RowHolder m_currentRow;
    }

      

As you can see, I only have one class that implements all the methods, and I really don't see a reason to implement many classes for each interface (so Ivaylo was trying to solve a problem that didn't exist for me;)). I think it's enough to have a state machine at the interface level, and the code can be much simpler and more compact.

... Other classes used in the code just to complete the image ...

    public interface ICommand 
    {
        //Apply func declaration
    }
    public class UpdateCommand : ICommand 
    {
        public UpdateCommand(TableHolder table) { /* ... */ }
        //Apply func implementation
    }
    public class TableHolder
    {
        public TableHolder(string tableName) { /* ... */ }
        public void AddRow(RowHolder row) { /* ... */ }
    }
    public class RowHolder
    {
        public RowHolder(long rowid) { /* ... */ }
        public void AddColumn(string columnName, Object columnValue) { /* ... */ }
    }
    public class Transaction : ITransaction
    {
        public Transaction(IEnumerable<ICommand> commands) { /* ... */ }
        //Execute func implementation
    }

      

Hope someone on the .NET team at Microsoft sees this and likes ...

+1


source


Update

I revisited the question, your last comments, and I came up with an idea based on my interpretation of your problem.

I agree with your last statement, there really won't be any difference between an interface Join<...>

and a hypothetical interface IOddAndPrimeCounter : IOddCounter, IPrimeCounter

. What I had in mind with point 2 is that you cannot distinguish whether the resulting connection interface should be called like T1

or T2

.

I would suggest a slightly different approach, which I myself use in such scenarios. I would keep the two interfaces IFirstColumnBuilder

and INextColumnBuilder

, but change the latter a bit:

interface INextColumnBuilder : 
    IFirstColumnBuilder, ITransactionBuilder, IRowBuilder, ITransactionHolder {}

      

Then I'll move on to one implementation IFirstColumnBuilder

and INextColumnBuilder

which looks like this:

public class ColumnBulder : INextColumnBuilder // IFirstColumnBuilder is already implemented
{
}

      

A typical implementation IRowBulder

would look like this:

public class RowBulder : IRowBulder
{
    public IFirstColumnBuilder Column(...)
    {
         // do stuff
         return new ColumnBuilder(...);
    }
}

      

The trick is that while ColumnBuilder

implements both IFirstColumnBuilder

, and the INextColumnBuilder

result from rowBuilder.Column()

will only have access to public methods IFirstColumnBuilder

.

I know this doesn't solve the interface problem very much, but still you have one single implementation, which means less code to write and less duplicate code on your interfaces. In addition, your call chain will be limited at will.


Original Answer

Since I had some thoughts on your question and I was a C # developer myself, I came up with some reasons why this probably won't happen in the CLR.

  • Your suggestion requires additional compile-time checking for T

    being an interface. In C #, generics can be subject to certain compile-time constraints. For example,

    public class ConstrainedList<T> : List<T> 
        where T: class, IEquatable<T>, new() { }
    
          

    The restriction will class

    restrict T

    only to reference types, the restriction IEquatable<T>

    will only allow types that implement the interface, IEquatable<T>

    and the restriction new()

    that T must have a public parameterless constructor. Multiple constraints, as in the example, must be executed immediately.

    Your suggestion is that the CLR should support the new constraint, let you call it interface

    , so it's T

    limited by the interface type. This constraint would be incompatible with the constraints new()

    and class

    would result in additional compile-time checks that CLR teams would need to implement. Perhaps if such a change is backward compatible or not, I think it won't, since both types class

    and value types hidden behind the interfaces behave like reference types. Thus, the restriction interface

    will act the same as the restrictionclass

    and yet allow value types that implement the interface to be considered acceptable, in other words, the constraint would contradict itself and could cause confusion and / or unexpected behavior in the code.

  • The second argument I have against this proposal is more practical. In C # and CLR languages ​​in general, you can explicitly implement interfaces. (If you don't know which is the explicit implementation of the interface and how it is used, I explained that at the end of the post). When you have this Join<T1, T2>

    interface, and the material from point 1 is implemented, then you will have problems when T1

    both T2

    provide a method with the same signature and require (and have) explicit implementations. So when you have this:

    IColumnBuilder columnBuilder = ...; // Assume a valid column builder is set
    var joinResult = columnBuilder.Column("col", "val");
    joinResult.ConflictingMethod(); // Oops!
    
          

    If it is ConflictingMethod()

    defined in both in T1

    and in T2

    what is joinResult

    inherited, but T1

    and are T2

    explicitly implemented, then in this line the compiler will not know which implementation ( T1

    or T2

    's) is called.

The above example may require more significant and non-dispatch changes to the CLR, if at all possible; so now it seems even less likely.


A note on explicit interfaces

This is a really useful feature if you have a class that needs to implement multiple interfaces, and at least two of the interfaces use a method with the same signature. The problem is that you may need to provide different implementations for each interface. Here's an example to illustrate:

    public interface IOddNumberCounter
    {
        int Count(params int[] input);
    }
    public interface IPrimeNumberCounter
    {
        int Count(params int[] input);
    }

      

Imagine a test that tests them:

    public void TestOddCounter(IOddNumberCounter counter)
    {
        AssertTrue(counter.Count(1, 2, 3, 15) == 3);
    }

    public void TestPrimeCounter(IPrimeNumberCounter counter)
    {
        AssertTrue(counter.Count(2, 3, 15, 16) == 2);
    }

      

Now, for some reason, you need this class:

    public class OddAndPrimeCounter : IOddNumberCounter, IPrimeNumberCounter
    {
        public int Count(params int[] input)
        {
            // Now what ?
        }
    }

      

The decision to pass tests for a class OddAndPrimeCounter

is to implement at least one (or both) conflicting methods explicitly. Explicit implementations have a slightly different syntax:

    public class OddAndPrimeCounter : IOddNumberCounter, IPrimeNumberCounter
    {
        int IOddNumberCounter.Count(params int[] input)
        {
            //count the odds
        }
        int IPrimeNumberCounter.Count(params int[] input)
        {
            //count the primes
        }
    }

      

0


source







All Articles