Unexpected behavior of a C# 8.0 default interface member
Asked Answered
J

1

20

Consider the following code:

interface I {
    string M1() => "I.M1";
    string M2() => "I.M2";
}

abstract class A : I {}

class C : A {
    public string M1() => "C.M1";
    public virtual string M2() => "C.M2";
}

class Program {
    static void Main() {
        I obj = new C();
        System.Console.WriteLine(obj.M1());
        System.Console.WriteLine(obj.M2());
    }
}

It produces the following unexpected output in .NET Core 3.1.402:

I.M1
C.M2

Class A has no implicit or explicit implementations of the members of I, so I would expect the default implementations to be used for C, because C inherits the interface mappings of A and does not explicitly re-implement I. According to ECMA-334 (18.6.6) and the C# 6.0 language specification:

A class inherits all interface implementations provided by its base classes.

Without explicitly re-implementing an interface, a derived class cannot in any way alter the interface mappings it inherits from its base classes.

In particular, I would expect the following output:

I.M1
I.M2

This is indeed what happens when A is not declared as abstract.

Is the behavior of the code above intended in C# 8.0, or is it a result of some bug? If intended, why does a method in C implicitly implement the respective member of I only when declared as virtual (in case of M2 but not M1) and only when A is declared as abstract?

EDIT:

While it is still unclear to me whether this is a bug or a feature (I tend to believe it is a bug, and the discussion linked in the first comment is inconclusive thus far), I came up with a much more dangerous scenario:

class Library {
    private interface I {
        string Method() => "Library.I.Method";
    }
    
    public abstract class A: I {
        public string OtherMethod() => ((I)this).Method();
    }
}

class Program {
    private class C: Library.A {
        public virtual string Method() => "Program.C.Method";
    }
    
    static void Main() {
        C obj = new C();
        System.Console.WriteLine(obj.OtherMethod());
    }
}

Note that the interface Library.I and the class Program.C are private to the respective classes. In particular, the method Program.C.Method should be inaccessible from outside the class Program. The author of class Program may believe to have full control of when the method Program.C.Method is called and may not even know of the interface Library.I (as it is private). However, it gets called from Library.A.OtherMethod, as the output is:

Program.C.Method

This looks like a kind of brittle base class problem. The fact that Program.C.Method is declared as public should be irrelevant. See Eric Lippert's this blog post, which describes a different but somewhat similar scenario.

Ja answered 10/10, 2020 at 15:42 Comment(3)
Asked on GitHubMining
Created a bug in dotnet/runtimeMining
@Mining Thanks for doing more investigation and submitting an appropriate bug report. Although I don't know runtime internals, I indeed tend to believe this is a bug—quite a serious one. I added a new scenario to my post which explains my point.Ja
D
4

Since the introduction of C# 8.0 the default implementation of an interface is supported. With this introduction the look up process for the implementing member has been changed for interfaces. The key part is on how the instance (in your example obj) is being defined, or type-syntax.

Lets start with the 7.3 ways of doing the member resolution and replace I obj = new C(); with C obj = new C(); When this is run the following output will be printed: C.M1 C.M2

As you can see both WriteLine's print the result as the implementation defined by class C. This is because the type-syntax refers to a class and the "first in line" implementation is that of class C.

Now when we change it back to I obj = new C(); we see different results, namely: I.M1 C.M2 This is because virtual and abstract members are not replaced with the most derived implementations as is the case with M1 (which is not marked virtual).

Now the main question still stands, why does a method in C implicitly implement the respective member of I only when declared as virtual (in case of M2 but not M1) and only when A is declared as abstract?

When class A is a non-abstract class, it's 'actively' implementating the interface while when it's an abstract class, the class is merely requiring the class that's inhereting the abstract class is also implementing the interface. When we look at your example we cannot write this:|

A obj = new C();

System.Console.WriteLine(obj.M1()); // Method M1() is not defined

For more information you can look here: https://github.com/dotnet/roslyn/blob/master/docs/features/DefaultInterfaceImplementation.md

Here are some variations with their results:

I obj = new C(); // with A as abstract class results in I.M1 C.M2

I obj = new C(); // with A as class results in I.M1 I.M2

C obj = new C(); // with or without A as abstract class results in C.M1 C.M2

I obj = new A(); // with A as class results in I.M1 I.M2

Dolly answered 11/10, 2020 at 17:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.