C#8 interface defaults: How to implement default properties in a nice and usable way
Asked Answered
C

1

11

I really loved the idea of default implementations on interfaces in C#8. But after trying it the disappointment was big...

So here's a simple example which I've found a part of the answer to in C#8 interfaces with properties/methods defined in them - apparently not working already why this is:

public interface IHasFirstNames
{
    string? FirstName => FirstNames.FirstOrDefault();

    List<string> FirstNames { get; }
}

public class Monster : IHasFirstNames
{
    public List<string> FirstNames { get; } = new();

    public Monster(params string[] firstNames)
    {
        FirstNames.AddRange(firstNames);
    }
}

public static class Program
{
    public static void Main()
    {
        var sally = new Monster("James", "Patrick");

        var error = sally.FirstName; // Cannot resolve symbol FirstName
        var works = ((IHasFirstNames)sally).FirstName;
    }
}

But whats the point of having a default implementation of a property in an interface if you always have to cast it ugly?!

So according to the casting-solution of above I've tried this:

public interface IHasFirstNames
{
    string? FirstName => FirstNames.FirstOrDefault();

    List<string> FirstNames { get; }
}

public class Monster : IHasFirstNames
{
    // Not ideal to declare the property here
    // But at least the implementation is still in the interface
    public string? FirstName => ((IHasFirstNames)this).FirstName;

    public List<string> FirstNames { get; } = new();

    public Monster(params string[] firstNames)
    {
        FirstNames.AddRange(firstNames);
    }
}

public static class Program
{
    public static void Main()
    {
        var sally = new Monster("James", "Patrick");

        var error = sally.FirstName; // StackOverflow!
    }
}

But against expectations this leads to a stack overflow as the cast to IHasFirstName does not really call the default implementation of the interface. Even when I implement a full getter with a dedicated variable of type IHasFirstName it leads to a stack overflow.

The only ugly solution I've come up with is this with a dedicated getter method:

public interface IHasFirstNames
{
    // Default implementation of a property is no use to me!
    string? FirstName { get; }
    // So I have to implement a getter method as default
    public string? FirstNameGetter() => FirstNames.FirstOrDefault();

    List<string> FirstNames { get; }
}

public class Monster : IHasFirstNames
{
    public string? FirstName => ((IHasFirstNames)this).FirstNameGetter();

    public List<string> FirstNames { get; } = new();

    public Monster(params string[] firstNames)
    {
        FirstNames.AddRange(firstNames);
    }
}

public static class Program
{
    public static void Main()
    {
        var sally = new Monster("James", "Patrick");

        var works= sally.FirstName;
    }
}

It doesn't have to be a method. Apparently it's also OK if its a property with a different name. Once the property in the interface and in the class should have the same name it gets ugly.

Is there really no nicer solution for this?

thanks

Chromatic answered 15/4, 2022 at 7:49 Comment(7)
If it's ugly, it means it's not used propertly. The whole point of using interfaces is to actually interact with them, not the actual class. Besides, what you post isn't a DIM issue - you'd have to do the same with any interface that was used through explicit implementation. DIMs are meant for versioning or traits. You'll find some example usages in the tag's detailsBiretta
An SDK can use default interface implementations to allow adding new interfaces and members without breaking existing code. That's used in Android, where many SDK interfaces use default interface members so applications don't have to implement all of them, or recompile for every SDK version and level. Different phones may provide different levels of support based on their hardware featuresBiretta
PS: The first snippet would work just fine if you used IHasFirstName sally = new Monster("James", "Patrick");Biretta
@PanagiotisKanavos: I've read the tag's detail and understand the uses of DIMs in such cases. Also your IHasFirstName sally = new Monster("James", "Patrick"); is clear - which is not practicle in real life as I loose the ability to access other properties of the class Monster via the instance sally. So it seems that DIMs are kinda named wrongly at least for my expectations. Then a "default interface member" should help me (DRY principle) not to do public string? FirstName => FirstNames.FirstOrDefault(); each and every time on all classes who implement IHasFirstName.Chromatic
In addition: What kinda bugs me is why ((IHasFirstNames)sally).FirstName works perfectly, but if I do the same inside the Monster class with ((IHasFirstNames)this).FirstName it does not work!Chromatic
The name isn't wrong - it's not even original. Java has default interface methods since Java 8 and I'd bet that (and the Android SDK) played a large part in introducing this feature: Xamarin and Unity are a very big .NET market. It's not meant for what you're doing - you're trying to use them as abstract classesBiretta
As for ((IHasFirstNames)this).FirstName you're calling your current class's implementation at that point, not the base class. public string? FirstName => implements the interface, so public string? FirstName =>((IHasFirstNames)this).FirstName; is calling itself. Again, you'd have the same issue with any method. Unless you explicitly implemented the member with public string? IHasFirstNames.FirstName ->.... To call the base implementation you'd need => base.FirstName which is planned but not implemented yetBiretta
P
2

As others have pointed out, this isn't really the intended usage for default interface methods. As the documentation states:

The most common scenario is to safely add members to an interface already released and used by innumerable clients.

For the way you want to use it, there's another mechanism available: Static extension methods. As you're probably already aware, this mechanism is used extensively in the CLR for things such as IEnumerable<T> extension methods.

For your case, you could include the following static extension class alongside the interface:

public static class HasFirstNamesExt
{
    public static string? FirstName(this IHasFirstNames self)
    {
        return self.FirstNames.FirstOrDefault();
    }
}

If you use that instead of the declaration in the interface itself, your code will work as expected.

However, this does of course suffer from the major drawback that implementing classes cannot change the implementation! If you want to support that, you need to use abstract base classes instead:

public static class Program
{
    public static void Main()
    {
        var sally = new Monster("James", "Patrick");
        Console.WriteLine(sally.FirstName); // "James"
    }
}

public interface IHasFirstNames
{
    List<string> FirstNames { get; }
    string? FirstName { get; }
}

public abstract class HasFirstNamesBase : IHasFirstNames
{
    public abstract List<string> FirstNames { get; }
    public virtual string? FirstName => FirstNames.FirstOrDefault();
}

public class Monster : HasFirstNamesBase
{
    public sealed override List<string> FirstNames { get; } = new();

    public Monster(params string[] firstNames)
    {
        FirstNames.AddRange(firstNames);
    }
}

Note that in this usage pattern, your implementing classes always derive from HasFirstNamesBase to make use of the default implementation.

Derived classes can override the implementation:

public class DifferentFirstNameImplementation: Monster
{
    public DifferentFirstNameImplementation(params string[] firstNames)
    :base (firstNames)
    {
    }

    public override string? FirstName => FirstNames.LastOrDefault();
}

Then:

public static void Main()
{
    var sally = new DifferentFirstNameImplementation("James", "Patrick");
    Console.WriteLine(sally.FirstName); // "Patrick"
}
Princely answered 15/4, 2022 at 10:13 Comment(1)
I know and use extension methods, although I kinda forgot I can write them for interfaces too. The reason I did go with an interface in the first place was I wasn't able to go with class hierarchy as I would need multi-inheritance for my full application. Besides the drawback of not being able to override the property in the class, which I (currently) don't need, and the difference of calling a method sally.FirstName() instead of a property sally.FirstName its quite a good solution for my purpose, thanks!Chromatic

© 2022 - 2024 — McMap. All rights reserved.