다중 상속의 정확한 문제는 무엇입니까?


121

여러 상속이 다음 버전의 C # 또는 Java에 포함되어야하는지 항상 묻는 사람들을 볼 수 있습니다. 이 능력을 가질만큼 운이 좋은 C ++ 사람들은 이것이 마치 누군가가 결국 스스로 매달릴 줄을주는 것과 같다고 말합니다.

다중 상속의 문제는 무엇입니까? 구체적인 샘플이 있습니까?


54
나는 C ++가 당신에게 충분한 로프를 줄 수 있다는 것을 언급하고 싶습니다.
tloach

1
다수의 동일한 문제를 해결하는 (그리고 IMHO가 해결하는) 다중 상속에 대한 대안은 Traits ( iam.unibe.ch/~scg/Research/Traits )
Bevan

52
나는 C ++가 발을 쏠 수있을만큼 충분한 로프를 제공한다고 생각했다.
KeithB

6
이 질문은 일반적으로 MI에 문제가 있다고 가정하는 것처럼 보이지만 MI가 일상적으로 사용되는 많은 언어를 발견했습니다. 특정 언어의 MI 처리에는 확실히 문제가 있지만 일반적으로 MI에 심각한 문제가 있다는 것을 알지 못합니다.
David Thornley

답변:


86

가장 명백한 문제는 함수 재정의입니다.

하자 두 개의 클래스가 있다고 가정 A하고 B하는 방법을 정의하는 두 가지 모두를 doSomething. 이제 및 C둘 다에서 상속되는 세 번째 클래스를 정의 하지만 메서드를 재정의하지 않습니다 .ABdoSomething

컴파일러가이 코드를 시드하면 ...

C c = new C();
c.doSomething();

... 방법의 어떤 구현을 사용해야합니까? 더 이상의 설명이 없으면 컴파일러가 모호성을 해결할 수 없습니다.

재정의 외에도 다중 상속의 또 다른 큰 문제는 메모리에있는 물리적 개체의 레이아웃입니다.

C ++, Java 및 C #과 같은 언어는 각 객체 유형에 대해 고정 주소 기반 레이아웃을 만듭니다. 이 같은:

class A:
    at offset 0 ... "abc" ... 4 byte int field
    at offset 4 ... "xyz" ... 8 byte double field
    at offset 12 ... "speak" ... 4 byte function pointer

class B:
    at offset 0 ... "foo" ... 2 byte short field
    at offset 2 ... 2 bytes of alignment padding
    at offset 4 ... "bar" ... 4 byte array pointer
    at offset 8 ... "baz" ... 4 byte function pointer

컴파일러가 기계어 코드 (또는 바이트 코드)를 생성 할 때 해당 숫자 오프셋을 사용하여 각 메서드 또는 필드에 액세스합니다.

다중 상속은 매우 까다 롭습니다.

클래스 CA및 둘 다에서 상속하는 경우 B컴파일러는 데이터를 AB순서대로 레이아웃할지 아니면 순서대로 레이아웃할지 결정해야합니다 BA.

그러나 이제 B객체 에 대해 메서드를 호출한다고 상상해보십시오 . 정말 그냥 B? 아니면 실제로 인터페이스를 C통해 다형 적으로 호출 되는 객체 B입니까? 개체의 실제 ID에 따라 물리적 레이아웃이 달라지며 호출 사이트에서 호출 할 함수의 오프셋을 알 수 없습니다.

이러한 종류의 시스템을 처리하는 방법은 고정 레이아웃 접근 방식을 버리고 함수를 호출하거나 해당 필드에 액세스 하기 전에 각 개체의 레이아웃을 쿼리 할 수 ​​있도록하는 것 입니다.

그래서 ... 긴 이야기를 짧게 ... 컴파일러 작성자가 다중 상속을 지원하는 것은 목에 고통입니다. 따라서 Guido van Rossum과 같은 사람이 파이썬을 설계하거나 Anders Hejlsberg가 c #을 설계 할 때 다중 상속을 지원하는 것이 컴파일러 구현을 훨씬 더 복잡하게 만들 것이라는 것을 알고 있으며, 아마도 그 혜택이 비용의 가치가 있다고 생각하지 않습니다.


62
Ehm, Python은 MI 지원
Nemanja Trifunovic

26
이것들은 그다지 설득력있는 주장이 아닙니다. 고정 된 레이아웃은 대부분의 언어에서 전혀 까다 롭지 않습니다. C ++에서는 메모리가 불투명하지 않기 때문에 까다롭기 때문에 포인터 산술 가정에 어려움을 겪을 수 있습니다. 클래스 정의 가 정적 인 언어 (Java, C # 및 C ++에서와 같이)에서는 여러 상속 이름 충돌이 컴파일 시간에 금지 될 수 있습니다 (그리고 C #은 인터페이스를 사용하여이를 수행합니다!).
Eamon Nerbonne

10
OP는 문제를 이해하고 싶어서 개인적으로 편집하지 않고 설명했습니다. 나는 언어 디자이너와 컴파일러 구현 자들이 "아마도 그 혜택이 비용의 가치가 있다고 생각하지 않는다"고 말했습니다.
benjismith

12
" 가장 명백한 문제는 함수 재정의입니다. "이것은 함수 재정의와는 아무 관련이 없습니다. 단순한 모호성 문제입니다.
curiousguy

10
Python이 MI를 지원하기 때문에이 답변에는 Guido 및 Python에 대한 잘못된 정보가 있습니다. "상속을 지원하는 한 단순한 버전의 다중 상속도 지원할 수 있다고 결정했습니다." — Guido van Rossum python-history.blogspot.com/2009/02/… — 게다가, 모호성 해결은 컴파일러에서 상당히 일반적입니다 (변수는 로컬에서 블록으로, 로컬에서 함수로, 로컬에서 함수로, 객체 멤버, 클래스 멤버, 전역 등), 추가 범위가 어떻게 차이를 만들지 모르겠습니다.
marcus

46

여러분이 언급 한 문제는 해결하기가 그리 어렵지 않습니다. 사실 에펠은 완벽하게 잘합니다! (임의의 선택 등을 도입하지 않고)

예를 들어 A와 B에서 모두 foo () 메서드를 사용하는 경우, 물론 A와 B 모두에서 상속하는 클래스 C에서 임의의 선택을 원하지 않습니다. foo를 재정의해야하므로 무엇이 될지 분명합니다. c.foo ()가 호출되거나 그렇지 않으면 C의 메소드 중 하나의 이름을 변경해야합니다. (bar ()가 될 수 있습니다.)

또한 다중 상속이 종종 매우 유용하다고 생각합니다. Eiffel 라이브러리를 보면 어디에서나 사용되고 있다는 것을 알 수 있으며 개인적으로 Java 프로그래밍으로 돌아 가야 할 때 기능을 놓쳤습니다.


26
나는 동의한다. 사람들이 MI를 싫어하는 주된 이유는 자바 스크립트 나 정적 타이핑을 사용하는 것과 같습니다. 대부분의 사람들이 MI를 아주 나쁜 구현으로 사용했거나 아주 나쁘게 사용했습니다. C ++로 MI를 판단하는 것은 PHP로 OOP를 판단하거나 Pintos로 자동차를 판단하는 것과 같습니다.
Jörg W Mittag

2
@curiousguy : MI는 C ++의 많은 "기능"과 마찬가지로 걱정해야 할 또 다른 복잡한 문제를 소개합니다. 모호하지 않다고해서 작업하거나 디버그하기가 쉽지 않습니다. 주제에서 벗어 났기 때문에이 사슬을 제거하고 어쨌든 그것을 날려 버렸습니다.
Guvante 2012 년

4
@Guvante 모든 언어에서 MI의 유일한 문제는 튜토리얼을 읽고 갑자기 언어를 알 수 있다고 생각하는 멍청한 프로그래머입니다.
Miles Rout

2
나는 언어 기능이 단지 코딩 시간을 줄이는 것이 아니라고 주장하고 싶습니다. 또한 언어의 표현력을 높이고 성능을 높이는 것입니다.
Miles Rout 2013 년

4
또한 버그는 바보가 잘못 사용하는 경우에만 MI에서 발생합니다.
Miles Rout 2013 년

27

다이아몬드 문제 :

두 클래스 B와 C가 A에서 상속하고 클래스 D가 B와 C 모두에서 상속 할 때 발생하는 모호함. A에 B와 C가 재정의 한 메서드 가 있고 D가 재정의하지 않는 경우 어떤 버전의 D가 상속하는 방법 : B 또는 C의 방법?

...이 상황에서 클래스 상속 다이어그램의 모양 때문에 "다이아몬드 문제"라고합니다. 이 경우 클래스 A는 상단에 있고 B와 C는 그 아래에 별도로 있으며 D는 하단에서 두 개를 결합하여 다이아몬드 모양을 형성합니다.


4
가상 상속이라는 솔루션이 있습니다. 잘못한 경우에만 문제가됩니다.
이안 Goldby

1
@IanGoldby : 가상 상속은 인스턴스가 파생되거나 대체 가능한 모든 유형 사이에서 신원 보존 업 캐스트 및 다운 캐스트를 허용 할 필요가없는 경우 문제의 일부를 해결하기위한 메커니즘입니다 . 주어진 X : B; Y : B; 및 Z : X, Y; someZ가 Z의 인스턴스라고 가정합니다. 가상 상속을 사용하면 (B) (X) someZ와 (B) (Y) someZ는 별개의 개체입니다. 중 주어진 하나는 풀이 죽은를 통해 다른 사람을 얻을 업 캐스팅 할 수도 있지만 무엇 하나가있는 경우 someZ와에 캐스팅하고 싶어 Object다음에 B? 어느 B것을 얻을 수 있습니까?
supercat dec.

2
@supercat 아마도 그런 문제는 대체로 이론적이며 어떤 경우에도 컴파일러에 의해 신호를받을 수 있습니다. 중요한 것은 해결하려는 문제를 인식하고 '왜?'를 이해하는 데 관심이없는 사람들의 교리를 무시하고 최상의 도구를 사용하는 것입니다.
이안 Goldby

@IanGoldby : 이와 같은 문제는 문제의 모든 클래스에 동시에 액세스 할 수있는 경우에만 컴파일러에서 신호를 보낼 수 있습니다. 일부 프레임 워크에서는 기본 클래스를 변경하면 항상 모든 파생 클래스를 다시 컴파일해야하지만 파생 클래스 (소스 코드가 없을 수도 있음)를 다시 컴파일하지 않고도 최신 버전의 기본 클래스를 사용할 수있는 기능은 유용한 기능입니다. 그것을 제공 할 수있는 프레임 워크를 위해. 게다가 문제는 단지 이론적 인 것이 아닙니다. .NET의 많은 클래스는 사실에 의존하는 어떤 기준으로 유형에서 캐스팅 Object및 유형에 다시 ...
supercat

3
@IanGoldby : 충분합니다. 제 요점은 Java와 .NET의 구현 자들이 일반화 된 MI를 지원하지 않기로 결정하는 데 단지 "게으른"것이 아니라는 것입니다. 일반화 된 MI를 지원하면 MI보다 유효성이 많은 사용자에게 더 유용한 다양한 공리를 프레임 워크에서 유지할 수 없었을 것입니다.
supercat dec

21

다중 상속은 자주 사용되지 않고 오용 될 수 있지만 때때로 필요한 것 중 하나입니다.

좋은 대안이 없을 때 오용 될 수 있다는 이유만으로 기능을 추가하지 않는 것을 이해하지 못했습니다. 인터페이스는 다중 상속의 대안이 아닙니다. 우선, 그들은 전제 조건이나 사후 조건을 강제 할 수 없습니다. 다른 도구와 마찬가지로 사용하기에 적합한시기와 사용 방법을 알아야합니다.


사전 및 사후 조건을 시행 할 수없는 이유를 설명해 주시겠습니까?
Yttrill 2011 년

2
@Yttrill은 인터페이스가 메소드 구현을 가질 수 없기 때문입니다. 어디에 넣 assert습니까?
curiousguy 2011

1
@curiousguy : 사전 및 사후 조건을 인터페이스에 직접 입력 할 수있는 적절한 구문을 가진 언어를 사용합니다. "어설 션"이 필요하지 않습니다. Felix의 예 : fun div (num : int, den : int when den! = 0) : int expect result == 0은 num == 0을 의미합니다.
Yttrill 2011

@Yttrill OK, 그러나 Java와 같은 일부 언어는 MI 또는 "인터페이스에 직접 사전 및 사후 조건"을 지원하지 않습니다.
curiousguy

그것은 사용할 수 없기 때문에 자주 사용되지 않으며 우리는 그것을 잘 사용하는 방법을 모릅니다. Scala 코드를 살펴보면 일이 어떻게 시작되고 특성으로 리팩토링 될 수 있는지 알 수 있습니다 (예, MI가 아니지만 내 요점을 증명 함).
santiagobasulto 2012

16

C에 의해 상속 된 객체 A와 B가 있다고 가정 해 봅시다. A와 B는 모두 foo ()를 구현하고 C는 그렇지 않습니다. C.foo ()를 호출합니다. 어떤 구현이 선택됩니까? 다른 문제가 있지만 이러한 유형은 큰 문제입니다.


1
그러나 그것은 실제로 구체적인 예가 아닙니다. A와 B가 모두 함수를 가지고 있다면 C도 자체 구현을 필요로 할 것입니다. 그렇지 않으면 자체 foo () 함수에서 A :: foo ()를 호출 할 수 있습니다.
Peter Kühne

@Quantum : 그렇지 않다면 어떨까요? 한 수준의 상속으로 문제를 쉽게 알 수 있지만 수준이 많고 어딘가에 두 배인 임의의 함수가 있으면 매우 어려운 문제가됩니다.
tloach

또한 요점은 원하는 것을 지정하여 메서드 A 또는 B를 호출 할 수 없다는 것이 아닙니다. 요점은 지정하지 않으면 하나를 선택할 좋은 방법이 없다는 것입니다. 나는 C ++이 이것을 어떻게 처리하는지 확실하지 않지만 누군가가 그것을 알고 있다면 그것을 언급 할 수 있습니까?
tloach

2
@tloach-C가 모호성을 해결하지 않으면 컴파일러는이 오류를 감지하고 컴파일 타임 오류를 반환 할 수 있습니다.
Eamon Nerbonne

@Earmon-다형성으로 인해 foo ()가 가상이면 컴파일러는 이것이 문제가 될 것임을 컴파일 타임에 알지 못할 수도 있습니다.
tloach

5

다중 상속의 주요 문제는 tloach의 예제로 잘 요약됩니다. 동일한 함수 또는 필드를 구현하는 여러 기본 클래스에서 상속 할 때 컴파일러는 상속 할 구현을 결정해야합니다.

동일한 기본 클래스에서 상속하는 여러 클래스에서 상속하면 더 나빠집니다. (다이아몬드 상속, 상속 트리를 그리면 다이아몬드 모양이됩니다)

이러한 문제는 컴파일러가 극복하는 데 실제로 문제가되지 않습니다. 그러나 여기서 컴파일러가 선택해야하는 것은 다소 임의적이므로 코드가 훨씬 덜 직관적입니다.

좋은 OO 디자인을 할 때 다중 상속이 필요하지 않습니다. 필요한 경우에는 일반적으로 상속을 사용하여 기능을 재사용하는 반면 상속은 "is-a"관계에만 적합합니다.

동일한 문제를 해결하고 다중 상속이 갖는 문제가없는 믹스 인과 같은 다른 기술이 있습니다.


4
컴파일 된 파일 임의의 선택을 할 필요 가 없습니다 . 단순히 오류가 발생할 수 있습니다. C #에서 유형은 ([..bool..]? "test": 1)무엇입니까?
Eamon Nerbonne

4
C ++에서 컴파일러는 이러한 임의의 선택을하지 않습니다. 컴파일러가 임의의 선택을해야하는 클래스를 정의하는 것은 오류입니다.
curiousguy

5

다이아몬드 문제가 문제가 아니라고 생각합니다.

내 관점에서 다중 상속과 관련된 최악의 문제는 RAD입니다. 피해자와 개발자라고 주장하지만 실제로는 절반의 지식 (기껏해야)에 갇혀있는 사람들입니다.

개인적으로 Windows Forms에서 다음과 같은 작업을 마침내 수행 할 수 있다면 매우 기쁠 것입니다 (올바른 코드는 아니지만 아이디어를 제공해야 함).

public sealed class CustomerEditView : Form, MVCView<Customer>

이것이 다중 상속이없는 주요 문제입니다. 인터페이스로 비슷한 일을 할 수 있지만 제가 "s *** 코드"라고 부르는 것이 있습니다. 예를 들어 데이터 컨텍스트를 얻기 위해 각 클래스에 작성해야하는 고통스러운 반복적 인 c ***입니다.

제 생각에는 현대 언어로 코드를 반복 할 필요가 전혀 없어야합니다.


나는 동의하는 경향이 있지만 단지 경향이 있습니다. 어떤 언어로든 실수를 감지하기 위해 중복성이 필요합니다. 그것이 핵심 목표이기 때문에 어쨌든 당신은 Felix 개발자 팀에 합류해야합니다. 예를 들어, 모든 선언은 상호 재귀 적이며 앞으로 및 뒤로 볼 수 있으므로 앞으로 선언 할 필요가 없습니다 (범위는 C goto 레이블과 같이 설정 방식입니다).
Yttrill 2011 년

나는 이것에 전적으로 동의한다 – 나는 여기서 비슷한 문제를 만났다 . 사람들은 다이아몬드 문제에 대해 이야기하고 종교적으로 인용하지만 제 생각에는 너무 쉽게 피할 수 있습니다 . (우리 모두가 iostream 라이브러리를 작성한 것처럼 프로그램을 작성할 필요는 없습니다.) 중복되는 함수 나 함수 이름이없는 서로 다른 두 기본 클래스의 기능이 필요한 객체가있을 때 다중 상속을 논리적으로 사용해야합니다. 오른손에는 도구입니다.
jedd.ahyoung 2011

3
@ 튜링 완료 : 코드 반복이없는 wrt : 좋은 생각이지만 부정확하고 불가능합니다. 엄청난 수의 사용 패턴이 있고 공통된 패턴을 라이브러리로 추상화하고 싶지만 모든 이름을 기억하는 의미 적 부하가 너무 높기 때문에 모든 패턴을 추상화하는 것은 광기입니다. 당신이 원하는 것은 좋은 균형입니다. 반복이 사물 구조를 제공한다는 것을 잊지 마십시오 (패턴은 중복성을 의미 함).
Yttrill 2011

@ lunchmeat317 : 일반적으로 '다이아몬드'가 문제를 일으키는 방식으로 코드를 작성해서는 안된다는 사실이 언어 / 프레임 워크 디자이너가 문제를 무시할 수 있다는 의미는 아닙니다. 프레임 워크가 업 캐스팅 및 다운 캐스팅이 객체 ID를 유지하도록 제공하는 경우, 클래스의 이후 버전이 주요 변경 사항없이 대체 할 수있는 유형의 수를 늘릴 수 있도록 허용하고 런타임 유형 생성을 허용하려는 경우, 위의 목표를 달성하면서 다중 클래스 상속 (인터페이스 상속과 반대)을 허용 할 수 없다고 생각합니다.
supercat 2012-08-30

3

CLOS (Common Lisp Object System)는 C ++ 스타일의 문제를 피하면서 MI를 지원하는 또 다른 예입니다. 상속에는 합리적인 기본값 이 주어 지면서 수퍼의 동작을 정확히 호출하는 방법을 명시 적으로 결정할 수있는 자유를 여전히 허용합니다. .


예, CLOS는 : 어쩌면 오랜 과거부터 현대 컴퓨팅의 개시 이후 가장 우수한 개체 시스템 중 하나입니다
rostamn739

2

다중 상속 자체에는 잘못된 것이 없습니다. 문제는 처음부터 다중 상속을 염두에두고 설계되지 않은 언어에 다중 상속을 추가하는 것입니다.

Eiffel 언어는 매우 효율적이고 생산적인 방식으로 제한없이 다중 상속을 지원하지만 처음부터이를 지원하도록 설계되었습니다.

이 기능은 컴파일러 개발자를 위해 구현하기 복잡하지만, 좋은 다중 상속 지원이 다른 기능의 지원을 피할 수 있다는 사실 (즉, 인터페이스 또는 확장 메소드가 필요 없음)으로 인해 단점을 보완 할 수있는 것 같습니다.

다중 상속을 지원하는지 여부는 선택의 문제, 우선 순위의 문제라고 생각합니다. 더 복잡한 기능은 올바르게 구현되고 작동하는 데 더 많은 시간이 걸리며 논란의 여지가 있습니다. C ++ 구현은 다중 상속이 C # 및 Java에서 구현되지 않은 이유 일 수 있습니다.


1
MI에 대한 C ++ 지원이 " 매우 효율적이고 생산적 "이지 않습니까?
curiousguy

1
실제로 C ++의 다른 기능과 맞지 않는다는 점에서 다소 깨졌습니다. 다중 상속은 말할 것도없고 상속에서는 할당이 제대로 작동하지 않습니다 (정말 나쁜 규칙을 확인하세요). 다이아몬드를 올바르게 생성하는 것은 너무나 어렵습니다. 표준위원회는이를 올바르게 수행하는 대신 단순하고 효율적으로 유지하기 위해 예외 계층 구조를 망쳤습니다. 이전 컴파일러에서 나는 이것을 테스트했을 때 사용하고 있었고 몇 가지 MI 믹스 인과 기본 예외의 구현은 메가 바이트의 코드를 넘어서고 .. 단지 정의를 컴파일하는 데 10 분이 걸렸습니다.
Yttrill 2011

1
다이아몬드가 좋은 예입니다. 에펠에서는 다이아몬드가 명시 적으로 해결됩니다. 예를 들어, Student와 Teacher가 모두 Person에서 물려 받았다고 상상해보십시오. 개인에게는 달력이 있으므로 학생과 교사 모두이 달력을 상속받습니다. Teacher와 Student 모두에서 상속되는 TeachingStudent를 만들어 다이아몬드를 만드는 경우 상속 된 캘린더 중 하나의 이름을 변경하여 두 캘린더를 별도로 사용할 수 있도록하거나 병합하여 Person처럼 작동하도록 결정할 수 있습니다. 다중 상속은 훌륭하게 구현 될 수 있지만 신중한 디자인이 필요합니다. 시작부터 가급적이면 ...
Christian Lemer

1
Eiffel 컴파일러는이 MI 모델을 효율적으로 구현하기 위해 글로벌 프로그램 분석을 수행해야합니다. 다형성 메서드 호출의 경우 여기에 설명 된대로 디스패처 썽크 또는 희소 행렬을 사용합니다 . 이것은 C ++의 개별 컴파일 및 C # 및 Java의 클래스 로딩 기능과 잘 섞이지 않습니다.
cyco130 2012

2

Java 및 .NET과 같은 프레임 워크의 디자인 목표 중 하나는 사전 컴파일 된 라이브러리의 한 버전과 함께 작동하도록 컴파일 된 코드가 후속 버전이더라도 해당 라이브러리의 후속 버전과 동일하게 잘 작동 할 수 있도록하는 것입니다. 새로운 기능을 추가하십시오. C 또는 C ++와 같은 언어의 일반적인 패러다임은 필요한 모든 라이브러리를 포함하는 정적으로 링크 된 실행 파일을 배포하는 것이지만 .NET 및 Java의 패러다임은 런타임에 "링크 된"구성 요소 모음으로 응용 프로그램을 배포하는 것입니다. .

.NET 이전의 COM 모델은이 일반적인 접근 방식을 사용하려고했지만 실제로는 상속이 없었습니다. 대신 각 클래스 정의는 모든 공용 멤버를 포함하는 동일한 이름의 클래스와 인터페이스를 모두 효과적으로 정의했습니다. 인스턴스는 클래스 유형이었고 참조는 인터페이스 유형이었습니다. 클래스가 다른 클래스에서 파생 된 것으로 선언하는 것은 다른 클래스의 인터페이스를 구현하는 것으로 클래스를 선언하는 것과 동일하며 새 클래스가 파생 된 클래스의 모든 공용 멤버를 다시 구현해야합니다. Y와 Z가 X에서 파생되고 W가 Y와 Z에서 파생되는 경우 Z가 해당 구현을 사용할 수 없기 때문에 Y와 Z가 X의 멤버를 다르게 구현하는지 여부는 중요하지 않습니다. 개인적인. W는 Y 및 / 또는 Z의 인스턴스를 캡슐화 할 수 있습니다.

Java 및 .NET의 어려움은 코드가 멤버를 상속 할 수 있고 멤버에 대한 액세스가 암시 적으로 상위 멤버를 참조 할 수 있다는 것 입니다. 위와 같이 WZ 클래스가 있다고 가정합니다.

class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z  // Not actually permitted in C#
{
  public static void Test()
  {
    var it = new W();
    it.Foo();
  }
}

W.Test()W의 인스턴스를 생성하면에 Foo정의 된 가상 메서드 구현을 호출해야하는 것처럼 보입니다 X. 그러나 Y와 Z가 실제로는 별도로 컴파일 된 모듈에 있고 X와 W가 컴파일 될 때 위와 같이 정의되었지만 나중에 변경되고 다시 컴파일되었다고 가정합니다.

class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }

이제 부름의 효과는 W.Test()무엇입니까? 프로그램이 배포 전에 정적으로 링크되어야한다면, 정적 링크 단계는 Y와 Z가 변경되기 전에 프로그램에 모호성이 없었지만 Y와 Z에 대한 변경으로 인해 일이 모호 해지고 링커가 거부 할 수 있음을 식별 할 수 있습니다. 그러한 모호성이 해결되지 않는 한 또는 그 때까지 프로그램을 빌드하십시오. 반면에, W와 Y 및 Z의 새 버전을 모두 가지고있는 사람은 단순히 프로그램을 실행하고 싶고 소스 코드가없는 사람 일 수 있습니다. W.Test()실행 하면 더 이상 명확하지 않습니다.W.Test() 하지만 사용자가 새 버전의 Y 및 Z로 W를 실행하려고 할 때까지 시스템의 어떤 부분도 문제가 있음을 인식 할 방법이 없습니다 (Y 및 Z로 변경되기 전에 W가 불법으로 간주되지 않는 한) .


2

다이아몬드는 C ++ 가상 상속과 같은 것을 사용 하지 않는 한 문제가되지 않습니다 . 일반적인 상속에서 각 기본 클래스는 멤버 필드와 유사합니다 (실제로는 이러한 방식으로 RAM에 배치됨). 더 많은 가상 메서드를 재정의하는 추가 기능. 이것은 컴파일 타임에 약간의 모호성을 부과 할 수 있지만 일반적으로 해결하기 쉽습니다.

반면에 가상 상속을 사용하면 너무 쉽게 제어 할 수 없게되고 엉망이됩니다. "하트"다이어그램을 예로 들어 보겠습니다.

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

C ++에서는 완전히 불가능합니다. FG단일 클래스로 병합 되 자마자 As도 병합됩니다. 당신 ++ 기본 클래스는 C에 불투명 고려하지 않을 수 있음을 의미합니다 (이 예에서는 구성 할 필요 AH당신이 알고 그래서 그것이 계층에 존재하는 곳). 그러나 다른 언어에서는 작동 할 수 있습니다. 예를 들어, FG명시 적으로 "내부"따라서 자신이 고체 만드는 결과의 합병을 금지하고 효과적으로 같이 선언 할 수있다.

또 다른 흥미로운 예 ( 하지 C ++ - 특정) :

  A
 / \
B   B
|   |
C   D
 \ /
  E

여기서는 B가상 상속 만 사용합니다. 그래서 E이 개 포함 B동일한 공유들 A. 이런 식으로를 A*가리키는 포인터를 얻을 EB*있지만 객체 실제로 B 그러한 캐스트가 모호한 경우 에도 포인터 로 캐스트 할 수 없으며 컴파일 타임에이 모호성을 감지 할 수 없습니다 (컴파일러가 전체 프로그램). 다음은 테스트 코드입니다.

struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};

int main() {
        E data;
        E *e = &data;
        A *a = dynamic_cast<A *>(e); // works, A is unambiguous
//      B *b = dynamic_cast<B *>(e); // doesn't compile
        B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
        std::cout << "E: " << e << std::endl;
        std::cout << "A: " << a << std::endl;
        std::cout << "B: " << b << std::endl;
// the next casts work
        std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
        std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
        std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
        std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
        return 0;
}

또한 구현이 매우 복잡 할 수 있습니다 (언어에 따라 다름, benjismith의 답변 참조).


그것이 MI의 진짜 문제입니다. 프로그래머는 한 클래스 내에서 다른 해상도가 필요할 수 있습니다. 언어 전체의 솔루션은 가능한 것을 제한하고 프로그래머가 프로그램이 올바르게 작동하도록 kludges를 만들도록 강제합니다.
shawnhcorey
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.