C# - 使用扩展方法提供默认接口实现

13

我正在学习 C# 扩展方法,想知道是否可以使用它为接口提供默认实现。

假设:

public interface Animal {
    string MakeSound();
}

public static string MakeSound(this Animal) {
    return "";
}

然后

public class Dog : Animal {
    string MakeSound() {
        return "Bark";
    }
}

public class Porcupine : Animal {
}

最后:

Animal dog = new Dog();
Animal porcupine = new Porcupine();

Print(dog.MakeSound());
Print(porcupine.MakeSound());

我希望豪猪和其他没有显式实现 MakeSound 的动物使用默认的扩展方法返回一个空字符串,但是狗和任何具有显式实现的动物将返回其自己的实现,例如“汪汪声”。

所以我的问题是: 1. 这可行吗? 2. 如果不行,是否有其他实现接口默认行为的方法?

抽象类而非接口不是一个选项,因为 C# 不支持多重继承,而我的类正在继承另一个类的行为。


6
扩展方法不能用来满足接口合约。 - p.s.w.g
好的,值得一试...考虑到我不想重新编写默认行为并且不能使用抽象类,您有其他建议吗? - tbkn23
1
为什么你不想要一个抽象类呢?抽象类本质上就是带有一些已经实现的方法的接口。然后你只需要为那些需要使用与基类不同实现的子类重写这些方法即可。 - Trent
1
因为C#中没有多重继承,而我需要让我的类继承另一个类... - tbkn23
同意,没有多重继承...但你可以链式继承。即类C继承自类B,类B继承自类A。 - Trent
1
这是不可能的,因为接口的所有实现都没有继承自同一个基类。 - tbkn23
3个回答

13

一般来说,我建议使用一个基础类。如果不行的话,你可以这样做:

public interface IAnimal { }

public interface INoisyAnimal : IAnimal {
    string MakeSound();
}

public static class AnimalExtensions { 
    public static string MakeSound(this IAnimal someAnimal) {
        if (someAnimal is INoisyAnimal) {
            return (someAnimal as INoisyAnimal).MakeSound();
        }
        else {
            return "Unknown Noise";
        }
    }
}

public class Dog : INoisyAnimal {
    public string MakeSound() {
        return "Bark";
    }
}

public class Porcupine : IAnimal { }

这使得每个 IAnimal 看起来像一个 INoisyAnimal 即使它并不是真正的 INoisyAnimal。例如:
IAnimal dog = new Dog();
IAnimal porcupine = new Porcupine();

Console.WriteLine(dog.MakeSound());            // bark
Console.WriteLine(porcupine.MakeSound());      // Unknown Noise

然而,这仍然不是接口的实际实现。请注意,尽管外观如此,但没有实际实现。
Console.WriteLine(porcupine is INoisyAnimal);  // false

另一种选择可能是创建一个包装器,在需要新功能时扩展您的基类:
public class NoisyAnimalWrapper : INoisyAnimal {
    private readonly IAnimal animal;
    public NoisyAnimalWrapper(IAnimal animal) {
        this.animal = animal;
    }

    public string MakeSound() {
        return "Unknown Noise";
    }
}

public static class AnimalExtensions { 
    public static INoisyAnimal Noisy(this IAnimal someAnimal) {
        return someAnimal as INoisyAnimal ?? 
                new NoisyAnimalWrapper(someAnimal);
    }
}

当需要时,您可以从任何IAnimal创建一个INoisyAnimal

INoisyAnimal dog = new Dog();
INoisyAnimal porcupine = new Porcupine().Noisy();

Console.WriteLine(dog.MakeSound());            // bark
Console.WriteLine(porcupine.MakeSound());      // Unknown Noise

你还可以将包装器设置为通用的(例如,NoisyAnimal<T> where T : IAnimal, new),并完全摒弃扩展方法。根据你实际的使用情况,这可能比以前的选项更可取。


我完全同意,如果适用的话,基类更好。关于你的第一个建议,由于扩展方法在编译时评估,即使它是INoisyAnimal的实现,dog.MakeSound()不会返回默认行为吗?编译器不知道dogINoisyAnimal类型,就其而言,将调用IAnimal.MakeSound()扩展方法。我错了吗? - tbkn23
@tbkn23 正确,这就是为什么你必须在扩展方法内部进行运行时检查。否则 ((IAnimal)new Dog()).MakeSound() 将返回 Unknown Noise - p.s.w.g
我同意使用基类来提供默认行为可能更好,但请记住,您的解决方案违反了Liskov替换原则,因为您的代码根据传递的实现而表现不同。 - Fabio Milheiro

2
我不确定你的具体情况,或者你只是在尝试实验,但如果只有一些动物很吵闹,那么这可能是一个很好的接口隔离的案例。
例如:
public class Dog : IAnimal, INoisy
{
    public string MakeSound()
    {
        return "Bark";
    }
}

public class Porcupine : IAnimal
{
}

然后,你只需要调用MakeSound或者那些确实会发出噪音的对象。

接口隔离可以使包装器设计复杂化。如果一个接口有许多可选方法和 canX 函数,那么编写一个包装对象,它将能够公开封装对象支持的函数,而不需要封装对象能够支持包装器用户可能不需要的函数也很容易。相比之下,如果给定实现接口组合的对象,则没有简单的方式来使包装器实现相同的接口组合。 - supercat
@supercat,非常感谢您的反馈。那让我思考了一下。不过,我们可不可以在不违反 SOLID 中的 L 的情况下尝试做到这一点呢?我在想,如果我们有一个列表,我们可以使用 animals.OfType<INoisy>().ForEach(a => a.MakeSound())。我见过一些非常糟糕的方法,试图理解底层实现,它们往往在每次更改时变得越来越丑陋。不过,我很好奇开发人员面临的真正问题是什么,因为它实际上可能需要完全不同的解决方案,而不是首先提出的问题建议的解决方案。 - Fabio Milheiro
我希望看到的方法是,一个框架允许每个接口X指定它应该被视为所有其他接口Y的实现,这样加载任何不实现X的其他接口Y的实现将自动生成X的每个成员链到X的静态成员。然后,接口INoisyAnimal:Animal可以包括属性CanMakeSound,默认实现返回false,以及函数MakeSound,默认实现抛出异常。 - supercat
包装类必须重写这些方法以便将它们传递给被包装的对象,但是一个单一的包装类就能够处理具有广泛能力的对象,而不需要那些对象的类为与该类无关的功能编写代码。如果默认实现仅限于在相关接口内定义,那么就可以避免致命钻石问题(Java添加了默认接口实现,但以更通用的方式进行,这使得致命钻石问题成为可能)。 - supercat

0
这样怎么样?它允许您避免使用基类,并且您可以按照您的想法进行操作,对吧?
public interface Animal
{
    // Fields
    string voice { get; }
}

public static class AnimalHelper
{
    // Called for any Animal
    public static string MakeSound(this Animal animal)
    {
        // Common code for all of them, value based on their voice
        return animal.voice;
    }
}

public class Dog : Animal
{
    public string voice { get { return "Woof!"; } }
}

public class Purcupine : Animal
{
    public string voice { get { return ""; } }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接