C#8 接口默认值:如何以良好且可用的方式实现默认属性

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

我真的很喜欢 C#8 中接口默认实现的想法。但试过之后,失望很大...

所以这是一个简单的例子,我已经在 中找到了答案的一部分,原因是:

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;
    }
}

但是,如果您总是不得不将其转换成丑陋的形式,那么在接口中使用 属性 的默认实现有什么意义呢?!

所以根据上面的铸造解决方案我已经试过了:

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; // Whosebug!
    }
}

但与预期相反,这会导致堆栈溢出,因为转换为 IHasFirstName 并没有真正调用接口的默认实现。 即使当我使用 IHasFirstName 类型的专用变量实现完整的 getter 时,它也会导致堆栈溢出。

我想出的唯一丑陋的解决方案是使用专用 getter 方法:

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;
    }
}

不一定是方法。显然,如果它是一个具有不同名称的 属性 也可以。一旦界面中的 属性 和 class 中的名称应该相同,它就会变得丑陋。

真的没有更好的解决方案吗?

谢谢

正如其他人所指出的,这并不是默认接口方法的实际用途。正如文档所述:

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

对于您想要的使用方式,还有另一种机制可用:静态扩展方法。您可能已经知道,这种机制在 CLR 中被广泛用于 IEnumerable<T> 扩展方法。

对于您的情况,您可以在界面旁边包含以下静态扩展 class:

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

如果您使用它而不是接口本身中的声明,您的代码将按预期工作。

但是,这当然会遇到实施 classes 无法更改实施的主要缺点!如果你想支持它,你需要使用 abstract base classes 代替:

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);
    }
}

请注意,在此使用模式中,您的实现 classes 始终派生自 HasFirstNamesBase 以使用默认实现。

派生的 classes 可以覆盖实现:

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

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

然后:

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