여러 상속이 다음 버전의 C # 또는 Java에 포함되어야하는지 항상 묻는 사람들을 볼 수 있습니다. 이 능력을 가질만큼 운이 좋은 C ++ 사람들은 이것이 마치 누군가가 결국 스스로 매달릴 줄을주는 것과 같다고 말합니다.
다중 상속의 문제는 무엇입니까? 구체적인 샘플이 있습니까?
여러 상속이 다음 버전의 C # 또는 Java에 포함되어야하는지 항상 묻는 사람들을 볼 수 있습니다. 이 능력을 가질만큼 운이 좋은 C ++ 사람들은 이것이 마치 누군가가 결국 스스로 매달릴 줄을주는 것과 같다고 말합니다.
다중 상속의 문제는 무엇입니까? 구체적인 샘플이 있습니까?
답변:
가장 명백한 문제는 함수 재정의입니다.
하자 두 개의 클래스가 있다고 가정 A
하고 B
하는 방법을 정의하는 두 가지 모두를 doSomething
. 이제 및 C
둘 다에서 상속되는 세 번째 클래스를 정의 하지만 메서드를 재정의하지 않습니다 .A
B
doSomething
컴파일러가이 코드를 시드하면 ...
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
컴파일러가 기계어 코드 (또는 바이트 코드)를 생성 할 때 해당 숫자 오프셋을 사용하여 각 메서드 또는 필드에 액세스합니다.
다중 상속은 매우 까다 롭습니다.
클래스 C
가 A
및 둘 다에서 상속하는 경우 B
컴파일러는 데이터를 AB
순서대로 레이아웃할지 아니면 순서대로 레이아웃할지 결정해야합니다 BA
.
그러나 이제 B
객체 에 대해 메서드를 호출한다고 상상해보십시오 . 정말 그냥 B
? 아니면 실제로 인터페이스를 C
통해 다형 적으로 호출 되는 객체 B
입니까? 개체의 실제 ID에 따라 물리적 레이아웃이 달라지며 호출 사이트에서 호출 할 함수의 오프셋을 알 수 없습니다.
이러한 종류의 시스템을 처리하는 방법은 고정 레이아웃 접근 방식을 버리고 함수를 호출하거나 해당 필드에 액세스 하기 전에 각 개체의 레이아웃을 쿼리 할 수 있도록하는 것 입니다.
그래서 ... 긴 이야기를 짧게 ... 컴파일러 작성자가 다중 상속을 지원하는 것은 목에 고통입니다. 따라서 Guido van Rossum과 같은 사람이 파이썬을 설계하거나 Anders Hejlsberg가 c #을 설계 할 때 다중 상속을 지원하는 것이 컴파일러 구현을 훨씬 더 복잡하게 만들 것이라는 것을 알고 있으며, 아마도 그 혜택이 비용의 가치가 있다고 생각하지 않습니다.
여러분이 언급 한 문제는 해결하기가 그리 어렵지 않습니다. 사실 에펠은 완벽하게 잘합니다! (임의의 선택 등을 도입하지 않고)
예를 들어 A와 B에서 모두 foo () 메서드를 사용하는 경우, 물론 A와 B 모두에서 상속하는 클래스 C에서 임의의 선택을 원하지 않습니다. foo를 재정의해야하므로 무엇이 될지 분명합니다. c.foo ()가 호출되거나 그렇지 않으면 C의 메소드 중 하나의 이름을 변경해야합니다. (bar ()가 될 수 있습니다.)
또한 다중 상속이 종종 매우 유용하다고 생각합니다. Eiffel 라이브러리를 보면 어디에서나 사용되고 있다는 것을 알 수 있으며 개인적으로 Java 프로그래밍으로 돌아 가야 할 때 기능을 놓쳤습니다.
다이아몬드 문제 :
두 클래스 B와 C가 A에서 상속하고 클래스 D가 B와 C 모두에서 상속 할 때 발생하는 모호함. A에 B와 C가 재정의 한 메서드 가 있고 D가 재정의하지 않는 경우 어떤 버전의 D가 상속하는 방법 : B 또는 C의 방법?
...이 상황에서 클래스 상속 다이어그램의 모양 때문에 "다이아몬드 문제"라고합니다. 이 경우 클래스 A는 상단에 있고 B와 C는 그 아래에 별도로 있으며 D는 하단에서 두 개를 결합하여 다이아몬드 모양을 형성합니다.
someZ
와에 캐스팅하고 싶어 Object
다음에 B
? 어느 B
것을 얻을 수 있습니까?
Object
및 유형에 다시 ...
다중 상속은 자주 사용되지 않고 오용 될 수 있지만 때때로 필요한 것 중 하나입니다.
좋은 대안이 없을 때 오용 될 수 있다는 이유만으로 기능을 추가하지 않는 것을 이해하지 못했습니다. 인터페이스는 다중 상속의 대안이 아닙니다. 우선, 그들은 전제 조건이나 사후 조건을 강제 할 수 없습니다. 다른 도구와 마찬가지로 사용하기에 적합한시기와 사용 방법을 알아야합니다.
assert
습니까?
C에 의해 상속 된 객체 A와 B가 있다고 가정 해 봅시다. A와 B는 모두 foo ()를 구현하고 C는 그렇지 않습니다. C.foo ()를 호출합니다. 어떤 구현이 선택됩니까? 다른 문제가 있지만 이러한 유형은 큰 문제입니다.
다중 상속의 주요 문제는 tloach의 예제로 잘 요약됩니다. 동일한 함수 또는 필드를 구현하는 여러 기본 클래스에서 상속 할 때 컴파일러는 상속 할 구현을 결정해야합니다.
동일한 기본 클래스에서 상속하는 여러 클래스에서 상속하면 더 나빠집니다. (다이아몬드 상속, 상속 트리를 그리면 다이아몬드 모양이됩니다)
이러한 문제는 컴파일러가 극복하는 데 실제로 문제가되지 않습니다. 그러나 여기서 컴파일러가 선택해야하는 것은 다소 임의적이므로 코드가 훨씬 덜 직관적입니다.
좋은 OO 디자인을 할 때 다중 상속이 필요하지 않습니다. 필요한 경우에는 일반적으로 상속을 사용하여 기능을 재사용하는 반면 상속은 "is-a"관계에만 적합합니다.
동일한 문제를 해결하고 다중 상속이 갖는 문제가없는 믹스 인과 같은 다른 기술이 있습니다.
([..bool..]? "test": 1)
무엇입니까?
다이아몬드 문제가 문제가 아니라고 생각합니다.
내 관점에서 다중 상속과 관련된 최악의 문제는 RAD입니다. 피해자와 개발자라고 주장하지만 실제로는 절반의 지식 (기껏해야)에 갇혀있는 사람들입니다.
개인적으로 Windows Forms에서 다음과 같은 작업을 마침내 수행 할 수 있다면 매우 기쁠 것입니다 (올바른 코드는 아니지만 아이디어를 제공해야 함).
public sealed class CustomerEditView : Form, MVCView<Customer>
이것이 다중 상속이없는 주요 문제입니다. 인터페이스로 비슷한 일을 할 수 있지만 제가 "s *** 코드"라고 부르는 것이 있습니다. 예를 들어 데이터 컨텍스트를 얻기 위해 각 클래스에 작성해야하는 고통스러운 반복적 인 c ***입니다.
제 생각에는 현대 언어로 코드를 반복 할 필요가 전혀 없어야합니다.
CLOS (Common Lisp Object System)는 C ++ 스타일의 문제를 피하면서 MI를 지원하는 또 다른 예입니다. 상속에는 합리적인 기본값 이 주어 지면서 수퍼의 동작을 정확히 호출하는 방법을 명시 적으로 결정할 수있는 자유를 여전히 허용합니다. .
다중 상속 자체에는 잘못된 것이 없습니다. 문제는 처음부터 다중 상속을 염두에두고 설계되지 않은 언어에 다중 상속을 추가하는 것입니다.
Eiffel 언어는 매우 효율적이고 생산적인 방식으로 제한없이 다중 상속을 지원하지만 처음부터이를 지원하도록 설계되었습니다.
이 기능은 컴파일러 개발자를 위해 구현하기 복잡하지만, 좋은 다중 상속 지원이 다른 기능의 지원을 피할 수 있다는 사실 (즉, 인터페이스 또는 확장 메소드가 필요 없음)으로 인해 단점을 보완 할 수있는 것 같습니다.
다중 상속을 지원하는지 여부는 선택의 문제, 우선 순위의 문제라고 생각합니다. 더 복잡한 기능은 올바르게 구현되고 작동하는 데 더 많은 시간이 걸리며 논란의 여지가 있습니다. C ++ 구현은 다중 상속이 C # 및 Java에서 구현되지 않은 이유 일 수 있습니다.
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가 불법으로 간주되지 않는 한) .
다이아몬드는 C ++ 가상 상속과 같은 것을 사용 하지 않는 한 문제가되지 않습니다 . 일반적인 상속에서 각 기본 클래스는 멤버 필드와 유사합니다 (실제로는 이러한 방식으로 RAM에 배치됨). 더 많은 가상 메서드를 재정의하는 추가 기능. 이것은 컴파일 타임에 약간의 모호성을 부과 할 수 있지만 일반적으로 해결하기 쉽습니다.
반면에 가상 상속을 사용하면 너무 쉽게 제어 할 수 없게되고 엉망이됩니다. "하트"다이어그램을 예로 들어 보겠습니다.
A A
/ \ / \
B C D E
\ / \ /
F G
\ /
H
C ++에서는 완전히 불가능합니다. F
및 G
단일 클래스로 병합 되 자마자 A
s도 병합됩니다. 당신 ++ 기본 클래스는 C에 불투명 고려하지 않을 수 있음을 의미합니다 (이 예에서는 구성 할 필요 A
에 H
당신이 알고 그래서 그것이 계층에 존재하는 곳). 그러나 다른 언어에서는 작동 할 수 있습니다. 예를 들어, F
및 G
명시 적으로 "내부"따라서 자신이 고체 만드는 결과의 합병을 금지하고 효과적으로 같이 선언 할 수있다.
또 다른 흥미로운 예 ( 하지 C ++ - 특정) :
A
/ \
B B
| |
C D
\ /
E
여기서는 B
가상 상속 만 사용합니다. 그래서 E
이 개 포함 B
동일한 공유들 A
. 이런 식으로를 A*
가리키는 포인터를 얻을 E
수 B*
있지만 객체 가 실제로 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의 답변 참조).