자기 유형과 특성 서브 클래스의 차이점은 무엇입니까?


387

형질의 자기 유형 A:

trait B
trait A { this: B => }

말한다 " A또한 연장하지 않는 구체적인 클래스로 함께 사용할 수 없습니다 B" .

반면에 다음은

trait B
trait A extends B

그 말한다 "어떤 (콘크리트 또는 추상) 클래스에 혼합 A또한 B에 혼합 될 것입니다" .

이 두 진술이 같은 의미가 아닙니까? self-type은 단순한 컴파일 타임 오류 가능성을 만드는 데만 사용됩니다.

내가 무엇을 놓치고 있습니까?


나는 실제로 자기 유형과 특성의 하위 분류의 차이점에 관심이 있습니다. 나는 자기 유형에 대한 일반적인 용도 중 일부를 알고 있습니다. 하위 유형 지정과 동일한 방식으로 더 명확하게 수행되지 않는 이유를 찾을 수 없습니다.
Dave

32
자체 유형 내에서 유형 매개 변수를 사용할 수 있습니다 : trait A[Self] {this: Self => }합법적 trait A[Self] extends Self입니다.
Blaisorblade

3
자체 유형은 클래스 일 수도 있지만 특성은 클래스에서 상속 할 수 없습니다.
cvogt

10
@cvogt : 특성은 클래스에서 상속받을 수 있습니다 (최소 2.10 이상) : pastebin.com/zShvr8LX
Erik Kaplun

1
@Blaisorblade : 작은 언어 재 설계로 해결할 수있는 것이 아닌 근본적인 한계는 없습니까? (적어도 문제의 관점에서)
Erik Kaplun

답변:


273

케이크 패턴과 같이 의존성 주입에 주로 사용됩니다 . 케이크 패턴을 포함하여 스칼라에는 다양한 형태의 의존성 주입을 다루는 훌륭한 기사 가 있습니다 . Google "케이크 패턴 및 스칼라"를 사용하면 프리젠 테이션 및 비디오를 포함한 많은 링크가 제공됩니다. 지금은 다른 질문에 대한 링크 입니다.

자아 유형과 특성을 확장하는 것의 차이점은 간단합니다. 당신이 말한다면 B extends A, 다음 B 입니다A . 당신이 자기 유형을 사용하는 경우, B 필요A. 자체 유형으로 작성되는 두 가지 특정 요구 사항이 있습니다.

  1. 경우 B확장, 당신은하고 필요한 혼합 된 위해 A.
  2. 구체적 클래스가 이러한 특성을 확장 / 혼합 할 때 일부 클래스 / 특성은 구현해야합니다 A.

다음 예를 고려하십시오.

scala> trait User { def name: String }
defined trait User

scala> trait Tweeter {
     |   user: User =>
     |   def tweet(msg: String) = println(s"$name: $msg")
     | }
defined trait Tweeter

scala> trait Wrong extends Tweeter {
     |   def noCanDo = name
     | }
<console>:9: error: illegal inheritance;
 self-type Wrong does not conform to Tweeter's selftype Tweeter with User
       trait Wrong extends Tweeter {
                           ^
<console>:10: error: not found: value name
         def noCanDo = name
                       ^

Tweeter의 서브 클래스 인 경우 User오류가 없습니다. 위의 코드에서, 우리는 필요한User할 때마다 Tweeter사용된다 그러나이 User제공되지 않았습니다 Wrong우리가 오류를 가지고, 그래서. 이제 위의 코드가 여전히 범위 내에 있으면 다음을 고려하십시오.

scala> trait DummyUser extends User {
     |   override def name: String = "foo"
     | }
defined trait DummyUser

scala> trait Right extends Tweeter with User {
     |   val canDo = name
     | }
defined trait Right 

scala> trait RightAgain extends Tweeter with DummyUser {
     |   val canDo = name
     | }
defined trait RightAgain

를 사용하면 Righta를 혼합해야하는 요구 사항 User이 충족됩니다. 그러나, 위에서 언급 한 두 번째 요구 사항이 충족되지 : 구현의 부담이 User여전히이 확장하는 클래스 / 형질 남아있다 Right.

함께 RightAgain두 요구 사항은 만족하고 있습니다. User및 구현 User이 제공된다.

보다 실용적인 사용 사례는이 답변의 시작 부분에있는 링크를 참조하십시오! 그러나, 이제 당신은 그것을 얻습니다.


3
감사. 케이크 패턴은 내가 자아 유형에 대한 과대 광고에 대해 이야기하는 이유의 90 %입니다.이 주제를 처음 본 곳입니다. Jonas Boner의 예는 내 질문의 요점을 강조하기 때문에 훌륭합니다. 히터 예제에서 자체 유형을 하위 특성으로 변경 한 경우 차이점은 무엇입니까 (올바른 항목을 혼합하지 않으면 ComponentRegistry를 정의 할 때 발생하는 오류 제외)
Dave

29
@ 데이브 : 당신은 같은 의미 trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent입니까? WarmerComponentImpl인터페이스 가 생길 수 있습니다. 그들은 확장 것도이 사용할 수있는 WarmerComponentImpl그대로, 분명 잘못, 아니 A는 SensorDeviceComponent,도 아니다 OnOffDeviceComponent. 자체 유형으로 이러한 종속성은 독점적 으로 사용할 수 있습니다 WarmerComponentImpl. A List는로 사용될 수 있으며 Array그 반대도 마찬가지입니다. 그러나 그들은 단지 같은 것이 아닙니다.
Daniel C. Sobral

10
고마워 다니엘. 이것은 아마도 내가 찾던 주요 차이점입니다. 실제 문제는 서브 클래 싱을 사용하면 원하지 않는 인터페이스로 기능이 누출된다는 것입니다. 그것은 특성에 대한보다 이론적 인 "일부"규칙을 위반 한 결과입니다. 자기 유형은 부품들 간의 "용도"관계를 나타냅니다.
Dave

11
@ 로드니 아니오, 안됩니다. 사실, this자체 유형과 함께 사용 하는 것은 내가 바라는 이유 this입니다. 왜냐하면 원래 이유는 아닙니다 .
Daniel C. Sobral

9
@opensas 사용해보십시오 self: Dep1 with Dep2 =>.
Daniel C. Sobral

156

자체 유형을 사용하면 주기적 종속성을 정의 할 수 있습니다. 예를 들어 다음을 달성 할 수 있습니다.

trait A { self: B => }
trait B { self: A => }

상속 extends은 그것을 허용하지 않습니다. 시험:

trait A extends B
trait B extends A
error:  illegal cyclic reference involving trait A

Odersky 책에서 33.5 (스프레드 시트 UI 생성 장) 섹션에서 다음 내용을 참조하십시오.

스프레드 시트 예제에서 Model 클래스는 Evaluator에서 상속되므로 평가 방법에 액세스 할 수 있습니다. 반대로, Evaluator 클래스는 다음과 같이 자체 유형을 Model로 정의합니다.

package org.stairwaybook.scells
trait Evaluator { this: Model => ...

도움이 되었기를 바랍니다.


3
나는이 시나리오를 고려하지 않았다. 내가 본 것 중 첫 번째 예는 서브 클래스와 마찬가지로 자체 유형과 동일하지 않습니다. 그러나 그것은 일종의 엣지 케이시처럼 보이고, 더 중요한 것은 나쁜 생각처럼 보입니다 (나는 일반적으로 순환 종속성을 정의하지 않는 방식으로 멀리 떨어져 있습니다!). 이것이 가장 중요한 차이점이라고 생각하십니까?
Dave

4
나도 그렇게 생각해. 필자가 self-types를 extends 절보다 선호하는 다른 이유는 없습니다. 자체 유형은 장황하므로 상속되지 않으며 (따라서 모든 하위 유형에 자체 유형을 의식으로 추가해야 함) 멤버 만 볼 수 있지만 재정의 할 수는 없습니다. 나는 케이크 패턴과 DI에 대한 자기 유형을 언급하는 많은 게시물을 잘 알고 있습니다. 그러나 나는 확신하지 못한다. 나는 여기에서 오래 전에 샘플 앱을 만들었습니다 ( bitbucket.org/mushtaq/scala-di ). 구체적으로 / src / configs 폴더를보십시오. 자체 유형없이 복잡한 Spring 구성을 대체하기 위해 DI를 달성했습니다.
Mushtaq Ahmed

Mushtaq, 우리는 동의합니다. 나는 의도하지 않은 기능을 노출시키지 않는다는 다니엘의 진술이 중요한 것이라고 생각하지만,이 기능을 무시하거나 미래의 서브 클래스에서 사용할 수 없다는 '기능'에 대한 견해가있다. 이것은 디자인이 언제 다른 것을 요구할 것인지를 명확하게 알려줍니다. 다니엘이 지적한 것처럼 객체를 모듈로 사용하기 시작하면 진정한 필요를 찾을 때까지 자기 유형을 피할 수 있습니다. 암시 적 매개 변수와 간단한 부트 스트 래퍼 객체를 사용하여 종속성을 자동 배선하고 있습니다. 나는 단순함을 좋아한다.
Dave

@ DanielC.Sobral은 귀하의 의견에 감사 할 수 있지만 현재 귀하의 의견보다 더 많은 찬성이 있습니다. 둘 다지지 :
rintcius

왜 하나의 특성 AB를 작성하지 않습니까? 특성 A와 B는 항상 마지막 클래스에서 결합되어야하는데 왜 처음부터 구분해야합니까?
Rich Oliver

56

또 다른 차이점은 자체 유형이 비 클래스 유형을 지정할 수 있다는 것입니다. 예를 들어

trait Foo{
   this: { def close:Unit} => 
   ...
}

여기서 자기 유형은 구조적 유형입니다. 그 결과 Foo에서 믹스되는 것은 arg가없는 "close"메소드 리턴 유닛을 구현해야한다는 것입니다. 이것은 오리 타이핑을위한 안전한 믹스 인을 허용합니다.


41
사실 당신은 구조 타입으로 상속을 사용할 수도 있습니다 : abstract class A extends {def close : Unit}
Adrian

12
나는 구조적 타이핑이 리플렉션을 사용한다고 생각하기 때문에 다른 선택이 없을 때만 사용하십시오.
Eran Medan

@Adrian, 귀하의 의견이 잘못되었다고 생각합니다. `abstract class A extends {def close : Unit}은 Object 수퍼 클래스가있는 추상 클래스입니다. 그것은 무의미한 표현에 대한 스칼라의 허용 구문 일뿐입니다. `class X extends {def f = 1}; 예를 들어 새로운 X (). f`
Alexey

1
@ Alexey 나는 왜 당신의 예 (또는 내)가 의미가 없는지 알지 못합니다.
Adrian

1
@Adrian abstract class A extends {def close:Unit}은에 해당합니다 abstract class A {def close:Unit}. 따라서 구조적 유형은 포함되지 않습니다.
Alexey

13

Martin Odersky의 원본 Scala paper Scalable Component Abstractions 의 2.3 절 "Selftype Annotations"는 실제로 믹스 인 구성을 넘어서는 셀프 타이프의 목적을 잘 설명하고 있습니다.

논문에 제시된 예는 다음과 같으며, 우아한 서브 클래스 특파원이없는 것 같습니다.

abstract class Graph {
  type Node <: BaseNode;
  class BaseNode {
    self: Node =>
    def connectWith(n: Node): Edge =
      new Edge(self, n);
  }
  class Edge(from: Node, to: Node) {
    def source() = from;
    def target() = to;
  }
}

class LabeledGraph extends Graph {
  class Node(label: String) extends BaseNode {
    def getLabel: String = label;
    def self: Node = this;
  }
}

서브 클래 싱이 왜이 문제를 해결하지 못하는지 궁금해하는 사람들을 위해 2.3 절에서는 이렇게 말합니다. 믹스 인 구성 메커니즘은 C_i가 추상 유형을 참조하도록 허용하지 않습니다. 이러한 제한으로 인해 클래스가 구성되는 시점에서 모호성을 정적으로 검사하고 충돌을 무시할 수 있습니다.”
Luke Maurer

12

언급되지 않은 또 다른 사항 : 자체 유형은 필수 클래스의 계층 구조의 일부가 아니므로 특히 봉인 된 계층 구조와 철저하게 일치하는 경우 패턴 일치에서 제외 될 수 있습니다. 다음과 같은 직교 동작을 모델링 할 때 편리합니다.

sealed trait Person
trait Student extends Person
trait Teacher extends Person
trait Adult { this : Person => } // orthogonal to its condition

val p : Person = new Student {}
p match {
  case s : Student => println("a student")
  case t : Teacher => println("a teacher")
} // that's it we're exhaustive

10

TL; 다른 답변의 DR 요약 :

  • 확장 한 형식은 상속 된 형식에 노출되지만 자체 형식은 그렇지 않습니다

    예 : class Cow { this: FourStomachs }와 같은 반추 동물 만 사용할 수있는 방법을 사용할 수 있습니다 digestGrass. 그러나 Cow를 확장하는 특성은 그러한 권한이 없습니다. 한편, 누구에게나 class Cow extends FourStomachs노출 digestGrass됩니다 extends Cow .

  • 자체 유형은 주기적 종속성을 허용하지만 다른 유형은 확장하지 않습니다.


9

주기적 의존성부터 시작하겠습니다.

trait A {
  selfA: B =>
  def fa: Int }

trait B {
  selfB: A =>
  def fb: String }

그러나이 솔루션의 모듈성은 처음 나타나는 것만 큼 좋지 않습니다. 자체 유형을 무시할 수 있기 때문입니다.

trait A1 extends A {
  selfA1: B =>
  override def fb = "B's String" }
trait B1 extends B {
  selfB1: A =>
  override def fa = "A's String" }
val myObj = new A1 with B1

자체 유형의 멤버를 대체하는 경우에도 상속을 사용하여 super를 통해 액세스 할 수있는 원래 멤버에 대한 액세스 권한이 손실됩니다. 따라서 상속을 통해 실제로 얻는 것은 다음과 같습니다.

trait AB {
  def fa: String
  def fb: String }
trait A1 extends AB
{ override def fa = "A's String" }        
trait B1 extends AB
{ override def fb = "B's String" }    
val myObj = new A1 with B1

이제 케이크 패턴의 모든 미묘함을 이해한다고 주장 할 수는 없지만, 모듈성을 적용하는 주된 방법은 상속이나 자기 유형보다는 구성을 통한 것입니다.

상속 버전은 짧지 만 자체 유형보다 상속을 선호하는 주된 이유는 자체 유형으로 초기화 순서를 올바르게 만드는 것이 훨씬 까다롭기 때문입니다. 그러나 상속으로 할 수없는 자기 유형으로 할 수있는 일이 있습니다. 상속에는 다음과 같이 특성이나 클래스가 필요하지만 자체 유형은 유형을 사용할 수 있습니다.

trait Outer
{ type T1 }     
trait S1
{ selfS1: Outer#T1 => } //Not possible with inheritance.

당신은 할 수 있습니다 :

trait TypeBuster
{ this: Int with String => }

당신이 그것을 인스턴스화 할 수는 없지만. 형식에서 상속 할 수없는 절대적인 이유는 보이지 않지만 형식 생성자 특성 / 클래스가 있으므로 경로 생성자 클래스와 특성을 갖는 것이 유용 할 것입니다. 불행히도

trait InnerA extends Outer#Inner //Doesn't compile

우리는 이것을 가지고 있습니다 :

trait Outer
{ trait Inner }
trait OuterA extends Outer
{ trait InnerA extends Inner }
trait OuterB extends Outer
{ trait InnerB extends Inner }
trait OuterFinal extends OuterA with OuterB
{ val myV = new InnerA with InnerB }

아니면 이거:

  trait Outer
  { trait Inner }     
  trait InnerA
  {this: Outer#Inner =>}
  trait InnerB
  {this: Outer#Inner =>}
  trait OuterFinal extends Outer
  { val myVal = new InnerA with InnerB with Inner }

더 공감해야 할 한 가지 점은 특성이 수업을 확장 할 수 있다는 것입니다. 이것을 지적 해 주신 David Maclver에게 감사드립니다. 내 코드의 예는 다음과 같습니다.

class ScnBase extends Frame
abstract class ScnVista[GT <: GeomBase[_ <: TypesD]](geomRI: GT) extends ScnBase with DescripHolder[GT] )
{ val geomR = geomRI }    
trait EditScn[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]
trait ScnVistaCyl[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]

ScnBaseSwing 프레임 클래스 에서 상속 되므로 자체 유형으로 사용 된 다음 마지막에 인스턴스화 할 수 있습니다. 그러나 val geomR특성을 상속하여 사용하기 전에 초기화해야합니다. 따라서 사전 초기화를 시행하는 클래스가 필요합니다 geomR. ScnVista그런 다음 클래스 는 자체에서 상속받을 수있는 여러 직교 특성으로 상속 될 수 있습니다. 여러 유형의 매개 변수 (일반)를 사용하면 다른 형태의 모듈성이 제공됩니다.


7
trait A { def x = 1 }
trait B extends A { override def x = super.x * 5 }
trait C1 extends B { override def x = 2 }
trait C2 extends A { this: B => override def x = 2}

// 1.
println((new C1 with B).x) // 2
println((new C2 with B).x) // 10

// 2.
trait X {
  type SomeA <: A
  trait Inner1 { this: SomeA => } // compiles ok
  trait Inner2 extends SomeA {} // doesn't compile
}

4

자체 유형을 사용하면 특성을 혼합 할 수있는 유형을 지정할 수 있습니다. 예를 들어, self type의 특성이있는 Closeable경우 해당 특성은 혼합 할 수있는 유일한 항목이 Closeable인터페이스 를 구현해야한다는 것을 알고 있습니다.


3
@Blaisorblade : kikibobo의 답변을 잘못 읽었는지 궁금합니다. 특성의 자기 유형은 실제로 혼합 할 수있는 유형을 제한 할 수 있으며 이는 유용성의 일부입니다. 예를 들어, 우리가 정의 trait A { self:B => ... }하면 선언 X with A이 X가 B를 확장하는 경우에만 유효합니다. 예, X with A with QQ는 B를 확장하지 않지만 kikibobo의 요점은 X가 너무 제한적이라고 생각합니다. 아니면 내가 뭔가를 그리워 했습니까?
AmigoNico

1
고마워요 투표가 잠겨 있었지만 다행히 답변을 수정 한 다음 투표를 변경할 수있었습니다.
Blaisorblade

1

업데이트 : 주요 차이점은 자체 유형이 여러 클래스에 따라 다를 수 있다는 것입니다 (약간의 경우입니다). 예를 들어

class Person {
  //...
  def name: String = "...";
}

class Expense {
  def cost: Int = 123;
}

trait Employee {
  this: Person with Expense =>
  // ...

  def roomNo: Int;

  def officeLabel: String = name + "/" + roomNo;
}

이를 통해 and Employee의 서브 클래스 인 모든 것에 믹스 인 을 추가 할 수 있습니다 . 물론 이것은 확장 되거나 그 반대의 경우에만 의미가 있습니다. 요점은 자체 유형을 사용 하는 것이 의존하는 클래스의 계층 구조와 독립적 일 수 있다는 것입니다. 무엇을 확장하는지는 중요하지 않습니다. vs 의 계층 구조를 전환하면 수정할 필요가 없습니다 .PersonExpenseExpensePersonEmployeeExpensePersonEmployee


직원이 Person의 하위 클래스 인 클래스 일 필요는 없습니다. 특성은 클래스를 확장 할 수 있습니다. Employee 특성이 자체 유형을 사용하는 대신 Person을 확장 한 경우에도 예제가 작동합니다. 나는 당신의 예제가 흥미 롭다는 것을 알지만, 자기 유형에 대한 유스 케이스를 설명하지는 않습니다.
Morgan Creighton

@MorganCreighton Fair, 나는 특성이 클래스를 확장 할 수 있다는 것을 몰랐습니다. 더 좋은 예를 찾을 수 있다면 그것에 대해 생각할 것입니다.
Petr Pudlák

예, 놀라운 언어 기능입니다. 만약 종업원이 종업원을 확장했다면 종업원이 "함께"포함 된 종업원도 종업원을 연장해야합니다. 그러나 Employee가 Person을 확장하는 대신 자체 유형을 사용하는 경우에도 이러한 제한이 존재합니다. 건배, Petr!
Morgan Creighton

1
나는 왜 "비용이 사람을 확장하거나 그 반대의 경우에만 의미가 있는지"알 수 없습니다.
Robin Green

0

첫 번째 경우, B의 하위 특성 또는 하위 클래스는 A를 사용하는 모든 것에 혼합 될 수 있습니다. 따라서 B는 추상 특성이 될 수 있습니다.


B는 두 경우 모두 "추상적 인 특성"이 될 수 있습니다. 따라서 그러한 관점과 차이가 없습니다.
Robin Green
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.