【问题标题】:Why do we need traits in scala?为什么我们需要 scala 中的特征?
【发布时间】:2016-10-12 03:29:44
【问题描述】:

所以,我试图制作一个 finagle 服务器,与哨兵交谈(不重要),并偶然发现了一个案例,我需要同时从两个 (不是特征)继承时间,让我们称它们为class SentryHandler extends Handlerclass TwitterHandler extends Handler,并假设我需要创建MyHandler,它继承自它们。

经过一阵愚蠢,当我认为不使用可怕的“委托模式”是不可能的时,我找到了解决方案:

trait SentryTrait extends SentryHandler
class MyHandler extends TwitterHandler with SentryTrait

现在,这让我想到:拥有“特质”概念的目的是什么?如果这个想法是强制你可以从多个特征继承但只能从一个类继承,那么它似乎非常容易绕过。听起来class 应该是继承的“主”行(您“扩展一个类具有特征”,但这也不是真的:你可以extend 一个带有(或不带)一堆其他特征的特征,而且根本没有类。

你不能实例化一个特征,但抽象类也是如此......

我能想到的唯一真正的区别是特征不能有构造函数参数。但这有什么意义呢? 我的意思是,为什么不呢?这样的事情会有什么问题?

class Foo(bar: String, baz: String) extends Bar(bar) with Baz(baz) 

【问题讨论】:

  • 主要思想是通过可以扩展类的实现来增强经典接口。没有看到太多要添加的内容..
  • @PavelOliynyk 好吧,这就是重点:已经有一个术语用于“通过实现增强的接口” - 它被称为“类”:)。所以,问题是为什么要创建一个新术语而不是使用现有术语。
  • 如果你的层次结构看起来像这样:trait Handler; class SentryHandler extends Handler; class TwitterHandler extends Handler; trait SentryTrait extends SentryHandler; class MyHandler extends TwitterHandler with SentryTrait 这不会在 REPL 上编译。
  • 文字游戏 :) 我想说的是每个人都必须接受的东西。
  • @YuvalItzchakov 在理论上,是的,但实际上...甚至没有任何意义)。 Option 是一个类,但 Map 是一个特征(“有地图”???)。 List 是一个类,但 Seq 是一个 trait ...... " 与 "has-a" 的困境,这是相当哲学的(在不好的意义上,因为没有实际意义)。

标签: scala traits


【解决方案1】:

您的解决方案(如果我理解正确的话) - 不起作用。您不能在 scala 中多继承类:

scala> class Handler
defined class Handler

scala> class SentryHandler extends Handler
defined class SentryHandler

scala> class TwitterHandler extends Handler
defined class TwitterHandler

scala> trait SentryTrait extends SentryHandler
defined trait SentryTrait

scala> class MyHandler extends TwitterHandler with SentryTrait
<console>:11: error: illegal inheritance; superclass TwitterHandler
 is not a subclass of the superclass SentryHandler
 of the mixin trait SentryTrait
       class MyHandler extends TwitterHandler with SentryTrait

至于问题 - 为什么特质,在我看来,这是因为特质是可堆叠的,以解决著名的diamond problem

  trait Base { def x: Unit = () }
  trait A extends Base { override def x: Unit = { println("A"); super.x}}
  trait B extends Base { override def x: Unit = { println("B"); super.x}}

  class T1 extends A with B {}
  class T2 extends B with A {}

  (new T1).x  // Outputs B then A
  (new T2).x  // Outputs A then B

即使 trait A super 是 Base(对于 T1)它调用 B 实现而不是 Base。这是由于trait linearization

所以对于类来说,如果你扩展一些东西——你可以确定接下来会调用这个基础。但这不适用于特征。这可能就是你没有特征构造函数参数的原因

【讨论】:

  • 哎呀,真可惜。我的 IDE 欺骗了我,让我相信继承会起作用 :( 太糟糕了。好吧,现在我知道特征与类的区别,但仍然不明白 为什么 它需要这样。线性化是一个很好的理论,但我看不出为什么不能将相同的过程应用于类。
  • @Dima 有一些语言,比如 Perl 或 Python,我相信他们做了你所说的事情。但我认为这使得使用类变得更加困难。你会想要避免线性化——因为它很复杂。我对 Perl 或 Python 不太熟悉,但我认为 Python 类总是有默认构造函数。继承时不能调用其他构造函数。这是因为这些问题。所以摆脱这些问题的自然方法 - 引入特征。
  • @Dima 关于这一切还有其他的事情。我不认为这是引入特征的原因,但它确实影响了决定。 Scala 基于 Java,没有多重继承。所以你不能在 Scala 中让类多继承
  • 我确定不是 name(trait vs. class)让你多继承 :) Scala 有一些巧妙的技巧来允许类从多个 trait 继承,并且仍然有效用java。我不明白在最后一句中用“class”替换“trait”这个词会增加或减少难度。
  • @Dima Scala 将特征编译到类中。你不能用类和多继承来做到这一点,并期望 Java 之后会看到这些类
【解决方案2】:

问题应该是:为什么我们需要 Scala 中的类? Martin Odersky 曾说过,Scala 可以仅靠特征就可以过关。我们需要向特征添加构造函数,以便可以构造特征的实例。没关系,Odersky 说他已经为 trait 构造函数制定了线性化算法。

真正的目的是平台互操作性。

Scala 打算集成的几个平台(目前是 Java,以前是 .NET,可能在未来的 Cocoa/Core Foundation/Swift/Objective-C)都有一个独特的类概念,而且并不总是那么容易拥有Scala 特征和平台类之间的 1:1 映射。例如,这与接口不同:平台接口和 Scala 特征之间存在一个微不足道的映射——只有抽象成员的特征与接口同构。

类、包和null 是主要用于平台集成的 Scala 功能的一些示例。

Scala 设计者非常努力地使语言保持小巧、简单和正交。但是 Scala 也明确地打算与现有平台很好地集成。事实上,尽管 Scala 本身是一门优秀的语言,但它专门被设计为主要平台语言(Java 平台上的 Java,.NET 平台上的 C#)的替代品。为了做到这一点,必须做出一些妥协:

  • Scala 有类,尽管它们与特征是冗余的(假设我们将构造函数添加到特征),因为将 Scala 类映射到平台类很容易,而将特征映射到平台类几乎是不可能的。看看 Scala 为将特征编译为高效的 JVM 字节码而必须跳过的环节。 (对于每个特征,都有一个包含 API 的接口和一个包含方法的静态类。对于混入特征的每个类,都会生成一个转发器类,它将对特征方法的方法调用转发到属于该特征的静态类特质。)
  • Scala 有包,尽管它们与对象是多余的。 Scala 包可以很容易地映射到 Java 包和 .NET 命名空间。物体不能。
  • 包对象是一种克服包的一些限制的方法,如果我们没有包,我们就不需要包对象。
  • 类型擦除。在编译到 JVM 时,完全可以保留泛型类型,例如您可以将它们存储在注释中。但是第三方Java库无论如何都会擦除它们的类型,并且其他语言不会理解注释并将Scala类型也视为已擦除,因此无论如何您都必须处理Type Erasure,如果无论如何都必须这样做,那为什么两者都做呢?
  • null,当然。在与真实世界的 Java 代码进行互操作时,不可能以任何理智的方式在 nullOption 之间自动映射。您必须在 Scala 中拥有 null,尽管我们宁愿希望它不存在。

【讨论】:

    【解决方案3】:

    在一个 trait(然后使它成为一个类)中具有构造函数和状态的问题在于多重继承。虽然这在技术上在假设的语言中是可行的,但对于语言定义和理解程序代码来说是很糟糕的。这个问题的其他回复中提到的菱形问题)导致最高级别的基类构造函数被调用两次(下面示例中的 A 的构造函数)。

    考虑使用允许多重继承的类 Scala 语言编写此代码:

    Class A(val x: Int)
    class B extends A(1)
    class C extends A(2)
    class D extends B, C
    

    如果包含状态,那么您必须在类 A 中有两个值 x 副本。所以您有两个 A 类副本(或一个副本和菱形问题 - 由于 UML 的菱形形状而被称为继承图)。

    Diamond Multiple Inheritance

    早期版本的 C++ 编译器(称为 C-Front)有很多错误,编译器或编译后的代码在处理这些错误时经常崩溃。问题包括如果您有对 B 或 C 的引用,您(实际上是编译器)如何确定对象的开始?编译器需要知道,为了将对象从 Base 类型(下图中或上图中的 A)转换为 Descendant 类型(上图中的 D)。

    Multiple Inheritance Memory Layout

    但是,这适用于特质吗?按照我的理解,Traits 是一种使用委托模式实现组合的简单方法(我假设你们都知道 GoF 模式)。当我们用任何其他语言(Java、C++、C#)实现委托时,我们会保留对其他对象的引用,并通过调用其类中的方法将消息委托给它。如果 Scala 内部通过简单地保留一个引用并调用它的方法来实现特征,那么特征的作用与委托完全相同。那么,为什么它不能有构造函数呢?我认为它应该能够在不违背其意图的情况下拥有一个。

    【讨论】:

    • 您可以像使用类一样使用特征继承状态和菱形继承,没有什么能阻止您这样做。的确,您不能在特征中拥有(非默认)构造函数,但我看不出它如何使任何事情变得更好。不,特征不是通过保留参考来实现的。它们是真正的类,由编译器动态生成。这根本不是委托模式,而是真正的继承。
    【解决方案4】:

    我能想到的唯一真正区别是特征不能有构造函数参数。但这有什么意义呢?我的意思是,为什么不呢?

    考虑

    trait A(val x: Int)
    trait B extends A(1)
    trait C extends A(2)
    class D extends B with C
    

    (new D {}).x 应该是什么?注意:Scala 3 中有plans to add trait parameters,但还是有限制的,所以上面是不允许的。

    【讨论】:

    • 你可以做trait A {def x: Int},然后有两个特征覆盖它并从两者继承。可以以相同的方式处理 Vals。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-09
    • 1970-01-01
    • 2015-07-10
    • 2014-06-18
    • 2017-02-26
    相关资源
    最近更新 更多