[C#] 파생 클래스에서 메서드를 호출하면 기본 클래스 메서드가 호출되는 이유는 무엇입니까?

이 코드를 고려하십시오.

class Program
{
    static void Main(string[] args)
    {
        Person person = new Teacher();
        person.ShowInfo();
        Console.ReadLine();
    }
}

public class Person
{
    public void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}
public class Teacher : Person
{
    public new void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

이 코드를 실행하면 다음이 출력됩니다.

나는 사람이다

그러나의 인스턴스가 Teacher아닌의 인스턴스임을 알 수 있습니다 Person. 왜 코드가 그렇게합니까?



답변

newvirtual/ 사이에는 차이가 있습니다 override.

인스턴스화 될 때 클래스는 포인터의 테이블에 지나지 않으며 실제 메소드 구현을 가리 킵니다. 다음 이미지는 이것을 잘 시각화해야합니다.

메소드 구현의 예

이제 방법이 정의 될 수있는 여러 가지 방법이 있습니다. 상속과 함께 사용될 때 각각 다르게 동작합니다. 표준 방식은 항상 위의 그림과 같이 작동합니다. 이 동작을 변경하려면 다른 키워드를 분석법에 첨부 할 수 있습니다.

1. 추상 수업

첫 번째는 abstract입니다. abstract방법은 단순히 아무데도 가리지 않습니다.

추상 클래스의 그림

클래스에 추상 멤버가 포함 된 경우으로 표시 abstract해야합니다. 그렇지 않으면 컴파일러가 애플리케이션을 컴파일하지 않습니다. abstract클래스의 인스턴스를 작성할 수는 없지만 클래스에서 상속하고 상속 된 클래스의 인스턴스를 작성하고 기본 클래스 정의를 사용하여 액세스 할 수 있습니다. 귀하의 예에서 이것은 다음과 같습니다.

public abstract class Person
{
    public abstract void ShowInfo();
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am a teacher!");
    }
}

public class Student : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am a student!");
    }
}

호출되면 ShowInfo구현에 따라 동작이 달라집니다.

Person person = new Teacher();
person.ShowInfo();    // Shows 'I am a teacher!'

person = new Student();
person.ShowInfo();    // Shows 'I am a student!'

둘, StudentS와 TeacherS는 PersonS,하지만 그들은 자신에 대해 프롬프트 정보를 묻는 메시지가 나타나면 서로 다른 동작합니다. 그러나 정보를 묻도록 요청하는 방법은 동일합니다. Person클래스 인터페이스 사용.

그래서 당신이 상속 할 때 무대 뒤에서 어떻게됩니까 Person? 구현할 때 ShowInfo포인터가 더 이상 아무 것도 가리 키지 않고 이제 실제 구현을 가리 킵니다. Student인스턴스를 만들 때 Students를 가리 킵니다 ShowInfo.

상속 된 메소드의 일러스트

2. 가상 방법

두 번째 방법은 방법을 사용 virtual하는 것입니다. 기본 클래스에서 선택적 기본 구현을 제공한다는 점을 제외하면 동작은 동일 합니다. 와 클래스 virtual멤버를 통해 인스턴스 할 수 있지만 상속 된 클래스는 다른 구현을 제공 할 수 있습니다. 코드가 실제로 작동하는 모습은 다음과 같습니다.

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am a person!");
    }
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am a teacher!");
    }
}

주요 차이점은 기본 멤버 Person.ShowInfo가 더 이상 아무 것도 가리 키지 않는다는 것입니다. 이것이 또한 인스턴스를 작성할 수있는 이유이기도합니다 Person(따라서 abstract더 이상 표시 하지 않아도 됨).

기본 클래스 내부의 가상 구성원 그림

이것은 현재 첫 번째 이미지와 다르지 않습니다. 그 이유는 virtual메소드가 ” 표준 방식 ” 구현을 가리키고 있기 때문 입니다. 사용하여 virtual, 당신은 말할 수 Persons그들은 것을, (안 한다 에 대해 서로 다른 구현을 제공) ShowInfo. 위와 override같이 다른 구현 (을 사용하여 ) 을 제공 Teacher하면 이미지는와 동일하게 보입니다 abstract. Students에 대한 사용자 정의 구현을 제공하지 않았다고 상상해보십시오 .

public class Student : Person
{
}

코드는 다음과 같이 호출됩니다.

Person person = new Teacher();
person.ShowInfo();    // Shows 'I am a teacher!'

person = new Student();
person.ShowInfo();    // Shows 'I am a person!'

그리고 이미지 Student는 다음과 같습니다.

가상 키워드를 사용하는 메소드의 기본 구현 그림

3. 마법의 ‘새로운’키워드 (일명 “그림자”)

new이 주위에 더 많은 해킹입니다. 기본 클래스 / 인터페이스의 메소드와 이름이 같은 일반화 된 클래스의 메소드를 제공 할 수 있습니다. 둘 다 자신의 사용자 정의 구현을 가리 킵니다.

새 키워드를 사용한 "주변"그림

구현은 제공 한 것과 유사합니다. 메소드에 액세스하는 방식에 따라 동작이 다릅니다.

Teacher teacher = new Teacher();
Person person = (Person)teacher;

teacher.ShowInfo();    // Prints 'I am a teacher!'
person.ShowInfo();     // Prints 'I am a person!'

이 동작을 원할 수는 있지만 오해의 소지가 있습니다.

이것이 당신을 위해 이해하기 쉽기를 바랍니다!


답변

C #의 하위 유형 다형성은 C ++과 유사하지만 Java와는 달리 명시 적 가상 성을 사용합니다. 즉, 메서드를 재정의 가능한 것으로 명시 적으로 표시해야합니다 (예 🙂 virtual. C #에서는 override오타를 방지하기 위해 재정의 메서드를 재정의 (예 :)로 명시 적으로 표시해야 합니다.

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

질문의 코드에서는 재정의 대신 그림자 를 사용 new하는 을 사용 합니다. 섀도 잉은 런타임 시맨틱이 아닌 컴파일 타임 시맨틱에만 영향을 미치므로 의도하지 않은 출력이됩니다.


답변

부모 클래스 참조에 넣은 클래스 객체의 메서드를 호출하려면 메서드를 가상 으로 만들어야하고 자식 클래스의 함수를 재정의해야합니다.

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}
public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

가상 방법

가상 메소드가 호출되면 오브젝트의 런타임 유형이 대체 멤버를 점검합니다. 파생 클래스가 멤버를 재정의하지 않은 경우 가장 파생 된 클래스의 재정의 멤버가 호출됩니다.이 멤버는 원래 멤버 일 수 있습니다. 기본적으로 메소드는 비 가상적입니다. 비가 상 방법을 재정의 할 수 없습니다. 정적, 추상, 개인 또는 대체 수정 자인 MSDN 과 함께 가상 수정자를 사용할 수 없습니다 .

섀도 잉에 New 사용

재정의 대신 새로운 키워드를 사용하고 있습니다. 이것이 새로운 기능입니다.

  • 파생 클래스의 메서드 앞에 new 또는 override 키워드가 없으면 컴파일러에서 경고를 표시하고 새 키워드가있는 것처럼 동작합니다.

  • 파생 클래스메서드 앞에 new 키워드가있는 경우이 메서드는 기본 클래스의 메서드와 독립적 인 것으로 정의됩니다 .이 MSDN 문서에서는 이를 잘 설명합니다.

초기 바인딩 VS 늦은 바인딩

컴파일 할 때 컴파일러가 객체 대신 참조 유형 (기본 클래스)의 기본 클래스의 메소드에 대한 호출을 바인딩 하는 현재의 경우 (가상이 아닌) 일반 메소드에 대한 초기 바인딩 이 있습니다. 클래스 즉, 파생 클래스 객체 . ShowInfo가상 방법 이 아니기 때문 입니다. 늦은 바인딩은 가상 메소드 테이블 (vtable)을 사용하여 런타임에 (가상 / 재정의 메소드) 수행됩니다 .

정상적인 함수의 경우 컴파일러는 메모리에서 숫자 위치를 계산할 수 있습니다. 그러면 함수가 호출 될 때이 주소에서 함수를 호출하는 명령을 생성 할 수 있습니다.

가상 메소드가있는 오브젝트의 경우 컴파일러는 v-table을 생성합니다. 이것은 본질적으로 가상 메소드의 주소를 포함하는 배열입니다. 가상 메서드가있는 모든 개체에는 v- 테이블의 주소 인 컴파일러에서 생성 된 숨겨진 멤버가 포함됩니다. 가상 함수가 호출되면 컴파일러는 v-table에서 적절한 방법의 위치를 ​​계산합니다. 그런 다음 객체 v-table에서 코드를 생성하고이 위치에서 가상 메소드를 호출합니다 ( Reference) .


답변

Achratt의 답변작성 하고 싶습니다 . 완전성을 위해, 차이점은 OP가 new파생 클래스의 메소드에서 키워드가 기본 클래스 메소드를 대체 할 것으로 예상한다는 것입니다. 실제로하는 것은 기본 클래스 메소드를 숨기는 것입니다.

C #에서 다른 답변이 언급했듯이 기존의 메서드 재정의는 명시 적이어야합니다. 기본 클래스 메소드는로 표시되어야 virtual하고 파생 클래스는 구체적 override으로 기본 클래스 메소드 여야합니다 . 이것이 완료되면 객체가 기본 클래스 또는 파생 클래스의 인스턴스로 취급되는지 여부는 중요하지 않습니다. 파생 된 메소드를 찾아서 호출합니다. 이것은 C ++에서와 비슷한 방식으로 수행됩니다. 컴파일 될 때 “가상”또는 “재정의”로 표시된 메소드는 참조 된 오브젝트의 실제 유형을 판별하고 변수 유형에서 실제 오브젝트 유형으로 트리를 따라 아래로 오브젝트 계층 구조를 순회하여 “지연”(런타임)으로 분석됩니다. 변수 유형으로 정의 된 메소드의 가장 파생 된 구현을 찾으십시오.

이것은 “암시 적 재정의”를 허용하는 Java와 다릅니다. 예를 들어 메소드 (정적이 아닌)의 경우, 동일한 서명 (이름 및 번호 / 유형의 매개 변수)의 메소드를 정의하면 서브 클래스가 수퍼 클래스를 대체합니다.

제어하지 않는 비가 상 메서드의 기능을 확장하거나 재정의하는 것이 유용한 경우가 많으므로 C #에는 new문맥 키워드 도 포함됩니다 . new대신을 재정의 키워드 “가죽”부모 방법. 상속 가능한 메소드는 가상이든 아니든 숨길 수 있습니다. 이를 통해 개발자는 그렇지 않은 멤버를 해결하지 않고도 부모로부터 상속하려는 멤버를 활용할 수 있으며 코드 소비자에게 동일한 “인터페이스”를 제공 할 수 있습니다.

숨기기는 숨기기 방법이 정의 된 상속 수준 이하에서 객체를 사용하여 사람의 관점에서 재정의하는 것과 유사하게 작동합니다. 질문의 예에서 Teacher를 만들고 Teacher 유형의 변수에 해당 참조를 저장하는 코더는 Teacher에서 ShowInfo () 구현의 동작을 볼 수 있습니다. 그러나 Person 레코드 컬렉션에서 객체를 사용하는 사람은 ShowInfo ()의 Person 구현 동작을 볼 수 있습니다. Teacher의 메소드는 부모 (Person.ShowInfo ()도 가상이어야 함)를 대체하지 않기 때문에 Person 추상화 레벨에서 작업하는 코드는 Teacher 구현을 찾지 못하고 사용하지 않습니다.

또한 new키워드가이를 명시 적으로 수행 할 뿐만 아니라 C #에서는 암시 적 메서드 숨기기를 허용합니다. override또는 없이 부모 클래스 메서드와 동일한 서명을 가진 메서드를 정의하면 메서드 new가 숨겨집니다 (ReSharper 또는 CodeRush와 같은 특정 리팩토링 도우미에서 컴파일러 경고 또는 불만 사항이 발생하지만). 이것이 C #의 디자이너가 C ++의 명시 적 재정의와 Java의 암시 적 재정의 사이에서 겪은 타협입니다. 우아하지만 우아하지만 이전 언어 중 하나의 배경에서 온 경우 항상 예상되는 동작을 생성하지는 않습니다.

새로운 내용은 다음과 같습니다 . 긴 상속 체인에서 두 키워드를 결합하면 복잡해집니다. 다음을 고려하세요:

class Foo { public virtual void DoFoo() { Console.WriteLine("Foo"); } }
class Bar:Foo { public override sealed void DoFoo() { Console.WriteLine("Bar"); } }
class Baz:Bar { public virtual void DoFoo() { Console.WriteLine("Baz"); } }
class Bai:Baz { public override void DoFoo() { Console.WriteLine("Bai"); } }
class Bat:Bai { public new void DoFoo() { Console.WriteLine("Bat"); } }
class Bak:Bat { }

Foo foo = new Foo();
Bar bar = new Bar();
Baz baz = new Baz();
Bai bai = new Bai();
Bat bat = new Bat();

foo.DoFoo();
bar.DoFoo();
baz.DoFoo();
bai.DoFoo();
bat.DoFoo();

Console.WriteLine("---");

Foo foo2 = bar;
Bar bar2 = baz;
Baz baz2 = bai;
Bai bai2 = bat;
Bat bat2 = new Bak();

foo2.DoFoo();
bar2.DoFoo();
baz2.DoFoo();
bai2.DoFoo();

Console.WriteLine("---");

Foo foo3 = bak;
Bar bar3 = bak;
Baz baz3 = bak;
Bai bai3 = bak;
Bat bat3 = bak;

foo3.DoFoo();
bar3.DoFoo();
baz3.DoFoo();
bai3.DoFoo();
bat3.DoFoo();

산출:

Foo
Bar
Baz
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat

첫 번째 다섯 세트는 모두 예상됩니다. 각 레벨에는 구현이 있으며 인스턴스화 한 것과 동일한 유형의 오브젝트로 참조되므로 런타임은 변수 유형이 참조하는 상속 레벨에 대한 각 호출을 분석합니다.

두 번째 5 세트는 각 인스턴스를 직계 상위 유형의 변수에 할당 한 결과입니다. 이제는 행동의 차이가 있습니다. foo2는 실제로 Bar캐스트 Foo된이며 실제 객체 유형 Bar의 파생 된 메소드를 계속 찾습니다. bar2A는 Baz하지만,과는 달리 foo2바즈가 명시 적으로 바의 구현을 재정의하지 않기 때문에, (이 할 수없는 바 sealed“하향식 (top-down)”를 찾고 때), 그것은 런타임에서 볼 수없는, 그래서 바의 구현 대신이라고합니다. Baz는 new키워드 를 사용할 필요가 없습니다 . 키워드를 생략하면 컴파일러 경고가 표시되지만 C #에서 암시적인 동작은 부모 메서드를 숨기는 것입니다. baz2A는 Bai, 어떤 재정 Baznew구현이므로 동작은와 비슷합니다 foo2. Bai에서 실제 객체 유형의 구현이 호출됩니다. bai2Bat부모 Bai의 메소드 구현 을 다시 숨기고 bar2Bai의 구현이 봉인되지 않은 것과 동일하게 작동 하므로 이론적으로 Bat이 메소드를 숨기는 대신 대체 할 수 있습니다. 마지막으로, bat2A는 Bak두 종류의 오버라이드 (override) 구현이없는, 단순히 부모의 것을 사용합니다.

세 번째 다섯 세트는 전체 하향식 해결 동작을 보여줍니다. 모든 것은 실제로 체인에서 가장 파생 된 클래스의 인스턴스를 참조 Bak하지만 모든 수준의 변수 유형에서 해결은 상속 체인의 해당 수준에서 시작하여 가장 파생 된 메서드의 명시 적 재정의로 드릴 다운하여 수행 됩니다. 이들의 Bar, Bai,와 Bat. 따라서 메소드 숨기기는 우선하는 상속 체인을 “파괴”합니다. 숨기기 방법을 사용하려면 메서드를 숨기는 상속 수준 또는 그 이하의 객체로 작업해야합니다. 그렇지 않으면 숨겨진 방법이 “발견”되어 대신 사용됩니다.


답변

C # : Polymorphism (C # Programming Guide)의 다형성에 대해 읽으십시오

다음은 그 예입니다.

새 키워드를 사용하면 교체 된 기본 클래스 멤버 대신 새 클래스 멤버가 호출됩니다. 이러한 기본 클래스 멤버를 숨겨진 멤버라고합니다. 파생 클래스의 인스턴스가 기본 클래스의 인스턴스로 캐스팅 된 경우에도 숨겨진 클래스 멤버를 호출 할 수 있습니다. 예를 들면 다음과 같습니다.

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.


답변

그것을 virtual만든 다음에서 해당 기능을 재정의해야합니다 Teacher. 파생 클래스를 참조하기 위해 기본 포인터를 상속하고 사용함에 따라를 사용하여 재정의해야합니다 virtual. 클래스 참조가 아닌 파생 클래스 참조 new에서 base클래스 메서드 를 숨기는 데 사용됩니다 base.


답변

이 정보를 확장하기 위해 몇 가지 예를 더 추가하고 싶습니다. 이것이 도움이되기를 바랍니다.

다음은 파생 유형이 기본 유형에 할당 될 때 발생하는 주변 환경을 정리하는 코드 샘플입니다. 이 컨텍스트에서 사용 가능한 메소드와 대체 된 메소드와 숨겨진 메소드의 차이점

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            a.foo();        // A.foo()
            a.foo2();       // A.foo2()

            a = new B();
            a.foo();        // B.foo()
            a.foo2();       // A.foo2()
            //a.novel() is not available here

            a = new C();
            a.foo();        // C.foo()
            a.foo2();       // A.foo2()

            B b1 = (B)a;
            b1.foo();       // C.foo()
            b1.foo2();      // B.foo2()
            b1.novel();     // B.novel()

            Console.ReadLine();
        }
    }


    class A
    {
        public virtual void foo()
        {
            Console.WriteLine("A.foo()");
        }

        public void foo2()
        {
            Console.WriteLine("A.foo2()");
        }
    }

    class B : A
    {
        public override void foo()
        {
            // This is an override
            Console.WriteLine("B.foo()");
        }

        public new void foo2()      // Using the 'new' keyword doesn't make a difference
        {
            Console.WriteLine("B.foo2()");
        }

        public void novel()
        {
            Console.WriteLine("B.novel()");
        }
    }

    class C : B
    {
        public override void foo()
        {
            Console.WriteLine("C.foo()");
        }

        public new void foo2()
        {
            Console.WriteLine("C.foo2()");
        }
    }
}

또 다른 작은 예외는 다음 코드 줄에 대한 것입니다.

A a = new B();
a.foo(); 

VS 컴파일러 (지능형)는 a.foo ()를 A.foo ()로 표시합니다.

따라서 더 파생 된 형식이 기본 형식에 할당 된 경우 파생 형식에서 재정의 된 메서드가 참조 될 때까지 ‘기본 형식’변수가 기본 형식으로 작동합니다. 이것은 부모와 자식 유형 사이에 동일한 이름을 갖지만 재정의되지 않은 숨겨진 메서드 또는 메서드를 사용하면 약간의 직관적이지 않을 수 있습니다.

이 코드 샘플은 이러한 경고를 설명하는 데 도움이됩니다!