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


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.