Click here to Skip to main content
15,891,902 members
Articles / Programming Languages / C#
Tip/Trick

Why C# interface inheritance makes sense: see LINQ...

Rate me:
Please Sign up or sign in to vote.
4.00/5 (3 votes)
22 May 2012CPOL3 min read 34.4K   7   10
How Linq extension methods benefit from interfaces inheriting from interfaces

Introduction

I came across the debate[^] if the following code is good or bad:

C#
public interface A { ... }
public interface B : A { ... }

This tip does not talk about the question if good or bad. I'm rather interested where that capability of C# has its unique value.

I was contemplating lately how Linq make the distinction between Linq-to-object and Linq-to-SQL. And really, that's where the interface inheritance plays its role: Linq as provided today would not be possible without.

The question

What makes the difference of the follwing two snippets:

C#
// Linq-to-object
int i = myList.Count(r=>r.Value==0);
// Linq-to-SQL
int j = myDbTable.Count(r=>r.Value==0);
I.e. how does C# decide that the first method is
Count<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
and the second is
Count<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);

Easy answer

Easy enough to make the decision, right?

  • The C# arrays and collections implement the IEnumerable<T> interface, so the respective extension method for IEnumerable<T> is taken.
  • The dbTable which is of type System.Data.Linq.Table<T> and which implements IQueryable<T> mandates the use of the respective extension method for IQueryable<T>.

Not so easy answer

But, wait! System.Data.Linq.Table<T> also implements IEnumerable<T>. Em, why does the C# compiler does not take the extension method for IEnumerable<T>?

How does the compiler make the right decision?

  • Is the sequence of the interfaces in the Table<T> class relevant?
  • Or is any other magic involved?

No, the sequence is not relevant.

The "magic" that is involved - you guess it - is that the IQueryable<T> interface inherits from IEnumerable<T> interface. This tells the compiler the the Table is "closer related" to IQueryable<T> than to IEnumerable<T>. Thus, the extension method for IQueryable<T> matches better.

Try it out in a sandbox!

For the purpose of this tip I've re-built very simplified interfaces and classes that show the basic relations of the relevant Linq classes.

You may play around with these and e.g. try to swap the interface sequence in DbTab class: no impact on the result.

Or you may replace the definition of IQu by

C#
public interface IQu /*: IEn*/ { }
I.e. IQu does not inherit any more from IEn: the result is a compilation error:
The call is ambiguous between the following methods or properties: 'QuExt.ExtCall(IQu)' and 'EnExt.ExtCall(IEn)'
C#
// Stands for System.Collections.Generic.IEnumerable<T>.
public interface IEn
{
    // Stands for the IEnumerable<T> interface function: GetEnumerator().
    int Count { get; } 
}
// Stands for System.Linq.IQueryable<T>: no interface functions.
// This exists only as distinct interface to IEnumerable<T>.
public interface IQu : IEn
{
}
// Stands for any C# collection or array like int[], IList<string>, etc.
public class CsArr : IEn
{
    // construction
    public CsArr(int size) { Count = size; }
    // implement interface
    public int Count { get; private set; }
}
// Stands for the System.Data.Linq.Table class (used as db table in linq-to-SQL).
public class DbTab : IQu, IEn
{
    // constructor
    public DbTab(int size) { Count = size; }
    // implement interface
    public int Count { get; set; }
}
// Stands for System.Linq.Enumerable extension class.
public static class EnExt
{
    // Stands for any Linq-to-object extension method. All these methods directly call the delegate.
    public static void ExtCall(this IEn e, Func<int, int> del)
    { Console.WriteLine("IEn: delegate({0}) = {1}", e.Count, del(e.Count)); }
}
// Stands for System.Linq.Queryable extension class.
public static class QuExt
{
    // Stands for any Linq-to-SQL (or Linq-to-QureyProvider) extension method.
    // All these methods are overloads to some of the Enumerable methods.
    // The Expression<...> arguments allow to translate the expression of the delegate into SQL.
    public static void ExtCall(this IQu e, Expression<Func<int, int>> del)
    {
        Console.WriteLine("IQu: Expression({0}): {1} => {2}", del, e.Count, del.Compile()(e.Count));
    }
}
public class Program
{
    public static void Main(string[] args)
    {
        var a = new CsArr(10); // Stands for a C# array or collection.
        var t = new DbTab(20); // Stands for a Linq db table.
        a.ExtCall(v=>v*v);     // both calls (direct delegate or Expression)...
        t.ExtCall(v=>v*v);     // ...are identical on client side
    }
}

The output is:

IEn: delegate(10) = 100
IQu: Expression(v => (v * v)): 20 => 400

Conclusion

This tip shows that inheritance of interfaces have a purpose in finding the best match in extension method overloads. This is obvious in the way Linq is implemented. This mechanism also makes sure that the best match on any overload (extension method or not) is taken.

I find it amazing that this simple feature enables the current Linq implementation (no matter whether design purists say that interfaces inheriting from interfaces is bad...).

And these Linq.Expression parameters are really great too - you get the actually passed expression that enables in the context of Linq to transform into SQL.

Side note: you pay a performance penalty with each level of abstraction:

  1. from x = v * v;
  2. over Func<int, int> f = (i) => i * i; x = f(v);
  3. to Expression<Func<int, int>> e = (i) => i * i; x = e.Compile()(v);

Where to go from here

History

V1.0, 2012-05-22, initial version
V1.1, 2012-05-22, fix some typo, add more elaborate example
V1.2, 2012-05-22, re-shuffled content, enhanced intro and sections, added references
V1.3, 2012-05-23, code got screwed up - restored

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Founder eXternSoft GmbH
Switzerland Switzerland
I feel comfortable on a variety of systems (UNIX, Windows, cross-compiled embedded systems, etc.) in a variety of languages, environments, and tools.
I have a particular affinity to computer language analysis, testing, as well as quality management.

More information about what I do for a living can be found at my LinkedIn Profile and on my company's web page (German only).

Comments and Discussions

 
Questionhow sequence in inheritance decides what to call Pin
raty4-Oct-12 12:11
raty4-Oct-12 12:11 
AnswerRe: how sequence in inheritance decides what to call Pin
Andreas Gieriet4-Oct-12 21:12
professionalAndreas Gieriet4-Oct-12 21:12 
GeneralFood for thought Pin
PIEBALDconsult22-May-12 17:50
mvePIEBALDconsult22-May-12 17:50 
GeneralRe: Food for thought Pin
Andreas Gieriet23-May-12 11:31
professionalAndreas Gieriet23-May-12 11:31 
GeneralRe: Food for thought Pin
PIEBALDconsult24-May-12 9:28
mvePIEBALDconsult24-May-12 9:28 
GeneralRe: Food for thought Pin
Andreas Gieriet24-May-12 22:20
professionalAndreas Gieriet24-May-12 22:20 
GeneralRe: Food for thought Pin
PIEBALDconsult25-May-12 3:01
mvePIEBALDconsult25-May-12 3:01 
GeneralRe: Food for thought Pin
Andreas Gieriet25-May-12 11:39
professionalAndreas Gieriet25-May-12 11:39 
The behaviour is still "correct" in the sense "as specified", in all aspects.

See the example below: I first show how it works with the implicit conversion from int to double and then the analogous behavior with the implicit conversion from int-0 to enum (see C# spec[^], section 13.1.3 page 136, implicit conversion from 0-int to any enum).

C#
public class Test1
{
    public interface I1      { void F(int i); }
    public interface I2 : I1 { void F(double d); }
    public class C: I2
    {
        public void F(int i) { Console.WriteLine("F(int)"); }
        public void F(double d) { Console.WriteLine("F(double)"); }
    }
    public static void Check()
    {
        I1 i1 = new C();
        i1.F(0); // F(int), the only matching function avaiable over that interface
        I2 i2 = new C();
        i2.F(0); // F(double) since in I2, that function is matching well enough through implicit conversion
    }
}
public class Test2
{
    public enum E { X };
    public interface I1      { void F(int i); }
    public interface I2 : I1 { void F(E d); }
    public class C : I2
    {
        public void F(int i) { Console.WriteLine("F(int)"); }
        public void F(E d) { Console.WriteLine("F(E)"); }
    }
    public static void Check0()
    {
        I1 i1 = new C();
        i1.F(0); // F(int), the only matching function avaiable over that interface
        I2 i2 = new C();
        i2.F(0); // F(E) since in I2, that function is matching well enough through implicit conversion
    }
    public static void Check1()
    {
        I1 i1 = new C();
        i1.F(1); // F(int), the only matching function avaiable over that interface
        I2 i2 = new C();
        i2.F(1); // F(int) since in I2 there is no function matching well enough through implicit conversion
    }
}

public static void Main(string[] args)
{
    Test1.Check();
    Test2.Check0();
    Test2.Check1();
    return;
}


Output:
F(int)
F(double)
F(int)
F(E)
F(int)
F(int)


You challenge the fourth line of the output. But it is in line with the second line: since in these interfaces there is a well enough matching method (by implicit conversion), no further serach is done for better match in base types/interfaces.

This int-0 to enum as specified in 13.1.3 is admittedly very odd. I would say, the spec is broken on that item. But the implementaiton is in line with that spec.

Cheers
Andi
GeneralRe: Food for thought Pin
PIEBALDconsult25-May-12 15:11
mvePIEBALDconsult25-May-12 15:11 
GeneralRe: Food for thought Pin
Andreas Gieriet29-May-12 19:53
professionalAndreas Gieriet29-May-12 19:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.