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

Sok*_*oko 10 c# interface c#-8.0 default-interface-member

我真的很喜欢 C#8 中接口默认实现的想法。但尝试之后却大失所望……

所以这是一个简单的例子,我在 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,如果你总是不得不把它改得丑陋,那么在接口中拥有属性的默认实现有什么意义呢?

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

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!
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我想出的唯一丑陋的解决方案是使用专用的 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

它不一定是一种方法。显然,如果它是一个不同名称的属性也没关系。一旦接口和类中的属性应该具有相同的名称,它就会变得很难看。

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

谢谢

Mat*_*son 2

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

最常见的场景是安全地将成员添加到已经发布并被无数客户端使用的接口中。

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

对于您的情况,您可以在接口旁边包含以下静态扩展类:

public static class HasFirstNamesExt
{
    public static string? FirstName(this IHasFirstNames self)
    {
        return self.FirstNames.FirstOrDefault();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

然而,这当然有一个主要缺点,即实现类不能改变实现!如果你想支持这一点,你需要使用抽象基类:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

派生类可以重写实现:

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

    public override string? FirstName => FirstNames.LastOrDefault();
}
Run Code Online (Sandbox Code Playgroud)

然后:

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