抽象类 vs. 接口 vs. Mixin

98

请问有人能给我解释一下抽象类接口混合类之间的区别吗?我以前在代码中使用过每种方式,但我不知道它们之间的技术差异。


2
重新标记为“oop”,因为它似乎比“concepts”标签更相关。 - Paul Morie
7个回答

104

抽象类

抽象类是一种不能被实例化的类。抽象类可以没有实现、部分实现或全部实现。抽象类旨在允许其子类共享一个通用(默认)实现。一个(伪代码)抽象类的例子可能如下所示:

abstract class Shape {
    def abstract area();  // abstract (unimplemented method)
    def outline_width() = { return 1; }  // default implementation
}

一个子类可能看起来像这样:
class Rectangle extends Shape {
    int height = width = 5;
    def override area() = { return height * width; }  // implements abstract method
    // no need to override outline_width(), but may do so if needed
}

可能的用法
def main() = {
    Shape[] shapes = { new Rectangle(), new Oval() };
    foreach (s in shapes) {
        print("area: " + s.area() + ", outline width: " + s.outline_width());
    }
}

如果一个子类没有覆盖未实现的方法,那么它也是一个抽象类。
接口
在计算机科学中,接口是程序向客户端公开的部分。公共类和成员是接口的示例。
Java和C#有一个特殊的“interface”关键字。这些关键字可以看作没有实现的抽象类。(关于常量、嵌套类、显式实现和访问修饰符还有一些技巧性问题,我就不再详细解释了。)虽然Java中“没有实现”的部分已经不适用了,但是添加了默认方法。“interface”关键字可以看作是接口概念的具体化。
回到Shape的例子。
interface Shape {
    def area();  // implicitly abstract so no need for abstract keyword
    def outline_width();  // cannot implement any methods
}

class Rectangle implements Shape {
    int height = width = 5;
    def override area() = { return height * width; }
    def override outline_width() = { return 1; }  // every method in interface must be implemented
}

def main() = {
    Shape[] shapes = { new Rectangle(), new Oval() };
    foreach (s in shapes) {
        print("area: " + s.area() + ", outline width: " + s.outline_width());
    }
}

Java和C#不允许具有实现的类进行多重继承,但它们允许多个接口实现。Java和C#使用接口作为解决方案来避免在允许多重继承的语言中发现的Deadly Diamond of Death Problem(如果处理得当,这并不是真正致命的)。
混入(有时称为特质)允许抽象类进行多重继承。混入没有多重继承所带来的可怕关联(由于C++的疯狂),因此人们更愿意使用它们。它们具有完全相同的Deadly Diamond of Death Problem,但支持它们的语言有比C++更优雅的缓解方式,因此它们被认为更好。
Mixins被誉为具有行为复用更灵活的接口和更强大的接口。您会注意到所有这些术语中都有interface一词,指的是Java和C#关键字。 Mixins不是接口。它们是多重继承。只是名字更好听了。

这并不是说mixins不好。多重继承并不坏。 C++解析多重继承的方式是让每个人都感到不安。

接下来是老掉牙的Shape示例

mixin Shape {
    def abstract area();
    def outline_width() = { return 1; }
}

class Rectangle with Shape {
    int height = width = 5;
    def override area() = { return height * width; }
}

def main() = {
    Shape[] shapes = { new Rectangle(), new Oval() };
    foreach (s in shapes) {
        print("area: " + s.area() + ", outline width: " + s.outline_width());
    }
}

您会注意到这个例子和抽象类的例子没有任何区别。
另一个小提示是,C# 从 3.0 版本开始支持 mixins。您可以在接口上使用扩展方法来实现。下面是 Shape 示例,采用真正的 C# 代码 mixin 风格。
interface Shape
{
    int Area();
}

static class ShapeExtensions
{
    public static int OutlineWidth(this Shape s)
    {
        return 1;
    }
}

class Rectangle : Shape
{
    int height = 5;
    int width = 5;

    public int Area()
    {
        return height * width;
    }
}

class Program
{
    static void Main()
    {
        Shape[] shapes = new Shape[]{ new Rectangle(), new Oval() };
        foreach (var s in shapes)
        {
            Console.Write("area: " + s.Area() + ", outline width: " + s.OutlineWidth());
        }
    }
}

14
明确一下:Mixin和Trait是扩展类功能的方式(例如添加方法),而不使用继承。回答暗示它们是相同的东西,但是有一些重要的区别。1. Mixin可能具有状态,Trait是无状态的2. Mixin使用隐式冲突解决,Trait使用显式冲突解决(更多信息请参见https://dev59.com/8XNA5IYBdhLWcg3whuV_) - Igor

22

一般来说:

接口是指定操作但没有任何实现的契约。一些语言(如Java、C#)内置了对接口的支持,而在其他语言中,“接口”描述了类似于C++中的纯虚类的约定。

抽象类是指定至少一个没有实现的操作的类。抽象类还可以提供它们实现的某些部分。同样,有些语言内置了将类标记为抽象的支持,而在其他语言中它是隐含的。例如,在C++中,定义了纯虚方法的类是抽象的。

混合类是一种设计用于使子类中某些功能的实现更容易的类,但不是设计用于单独使用的。例如,假设我们有一个处理请求对象的接口。

interface RequestHandler {
  void handleRequest(Request request);
}

也许将请求缓冲起来,累积它们直到达到某个预定数量再刷新缓冲区是有用的。我们可以通过使用mixin实现缓冲功能,而不必指定刷新行为:

abstract class BufferedRequestHandlerMixin implements RequestHandler {
  List<Request> buffer = new List<Request>();

  void handleRequest(Request request) {
    buffer.add(request);

    if (buffer.size == BUFFER_FLUSH_SIZE) {
        flushBuffer(buffer);
        buffer.clear();
    }
  }

  abstract void flushBuffer(List<Request> buffer);
}

这种方式使我们能够轻松编写一个请求处理程序,将请求写入磁盘、调用Web服务等,而无需每次重写缓冲功能。 这些请求处理程序可以简单地扩展BufferedRequestHandlerMixin并实现flushBuffer

另一个很好的混合类示例是Spring中的许多支持类之一,即HibernateDaoSupport


太好了,这正是我所需要的,谢谢。不过有一件事:您能否明确 mixins 如何“使子类中某些行为的实现更容易”? - Sasha Chedygov
请告诉我这个例子是否有帮助。 - Paul Morie
2
等等,也许我漏掉什么了,但是BufferedRequestHandlerMixin不是一个抽象类吗?这与Mixin有什么不同? - mR_fr0g
mR_fr0g,是的,BufferedRequestHandlerMixin被实现为抽象类。在Java中,通常使用抽象类实现混合类,因为抽象关键字表示该类设计用于重用,而不是单独使用(它还可以防止您单独使用它)。 - Paul Morie
“抽象类是指至少规定了一个没有实现的操作的类”:对于.NET来说并不完全正确。例如,以下是C#中有效的类定义:“public abstract class Foo { }”。 - Darin Dimitrov

8

由于很多人已经解释了定义和用法,我只想强调几个重要点。

接口:

  1. 定义一个契约(最好是无状态的 - 即没有变量)
  2. 使用“具有”功能将不相关的类链接在一起。
  3. 声明公共常量变量(不可变状态)

抽象类:

  1. 在几个密切相关的类之间共享代码。它建立了“是一个”关系。

  2. 在相关类之间共享公共状态(状态可以在具体类中修改)

我将通过一个小例子来解释它们之间的区别。

Animal 可以是一个抽象类。CatDog 扩展这个抽象类建立了“是一个”关系。

Cat 是 Animal

Dog 是 Animal.

Dog 可以 实现 Bark 接口。然后 Dog 具有 Bark 的能力。

Cat 可以 实现 Hunt 接口。然后 Cat 具有 Hunting 的能力。

Man,不是 Animal,可以实现 Hunt 接口。然后 Man 具有 Hunting 的能力。

Man 和 Animal(Cat/Dog)没有关联。但是 Hunt 接口可以为不相关的实体提供相同的能力。

Mixin:

  1. 如果您想要抽象类和接口的混合。当您希望在许多不相关的类上强制执行新的契约时,特别有用,其中一些类必须重新定义新行为,而另一些类应该坚持共同的实现。在 Mixin 中添加公共实现,并允许其他类重新定义合同方法(如果需要)

如果我想声明一个抽象类,我将遵循以下两种方法之一。

  1. Move all abstract methods to interface and my abstract class implements that interface.

    interface IHunt{
        public void doHunting();
    }
    abstract class Animal implements IHunt{
    
    }
    class Cat extends Animal{
        public void doHunting(){}
    }
    

相关的SE问题:

接口和抽象类有什么区别?


6
参考Java并给出抽象类提供mixin的示例是误导性的。首先,Java不支持默认的“mixins”。在Java术语中,“抽象类”和“Mixins”变得混淆。
Mixin是一个类型,一个类可以实现它作为其“主要类型”的附加功能。换句话说,在Java术语中,一个例子是您的业务值对象实现Serializable。
Josh Bloch说:“由于类不能有多个父类,抽象类不能用于定义mixins”(请记住,Java只允许一个“extends”候选项)。
寻找像Scala和Ruby这样的语言以适当地实现“mixin”的概念。

此外,JavaScript是最广泛使用的语言之一,具有混合概念。 - Gaurav Ramanan
1
在Java 8版本中,接口中的默认方法提供了混合特性。 - Ravindra babu

4
"Mixin"的含义由约书亚·布洛赫在他的《Effective Java》一书中清晰地定义。以下是摘自同一书籍的一段引述:
“Mixin”是指类除其“主要类型”外可以实现的一种类型,它声明该类提供了一些可选行为。例如,“Comparable”是一个mixin接口,允许一个类声明其实例与其他可相互比较的对象有序。这样的接口被称为“mixin”,因为它允许可选功能被“混合”到类型的主要功能中。

3

抽象类基本上就是带有一些具体实现的接口。而接口只是指定了一份没有实现细节的契约。

如果您想要在实现抽象类的所有对象之间创建共同功能,则可以使用抽象类。这符合面向对象编程中DRY(不要重复自己)原则。


3
不只如此。例如,抽象类可以定义抽象受保护方法,接口则不行。你可以实现任意数量的接口,但只能继承一个抽象类(除非你的语言支持多重继承)。 - n3rd
是的,肯定还有比我描述的更多(对于@musicfreak,你应该进行更多研究,因为这个主题有很多内容)。只是试图为OP提供一个简单的定义,因为他/她表示想要一些不太技术性的东西。 - Jon Erickson
+1 简洁明了,感谢 n3rd 提供的信息。 - Sasha Chedygov

1

抽象类是一种类,其中并非所有成员都被实现,而是留给继承者来实现。它强制其继承者实现其抽象成员。 抽象类不能被实例化,因此它们的构造函数不应该是公共的。

以下是C#中的一个示例:

    public abstract class Employee
    {
        protected Employee(){} 
        public abstract double CalculateSalary(WorkingInfo workingInfo);//no implementation each type of employee should define its salary calculation method.
    }

   public class PartTimeEmployee:Employee
  {
    private double _workingRate;
    public Employee(double workingRate)
    {
     _workingRate=workingRate;
    }
    public override double CalculateSalary(WorkingInfo workingInfo)
    {
      return workingInfo.Hours*_workingRate;
    }

}

接口是一个类要实现的契约。它只声明了实现类成员的签名,本身没有实现。我们通常使用接口来实现多态,并解耦相关的类。

以下是C#中的一个示例:

public interface IShape
{
int X{get;}
int Y{get;}
void Draw();
}

public class Circle:IShape
{
public int X{get;set;}
public int Y{get;set;}

public void Draw()
{
//Draw a circle
}
}

public class Rectangle:IShape
{
public int X{get;set;}
public int Y{get;set;}

public void Draw()
{
//Draw a rectangle
}
}

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