C #이 Java와 달리“new”및“virtual + override”키워드로 작성된 이유는 무엇입니까? 생각해야하지만. 어느 정도 찬반 점이 있습니다 (지금

자바에서는 더있다 virtual, new, override방법 정의에 대한 키워드. 따라서 방법의 작동은 이해하기 쉽습니다. 원인 DerivedClass가 BaseClass를 확장 하고 동일한 이름과 동일한 BaseClass 서명을 가진 메소드를 갖는 경우 대체는 런타임 다형성에서 수행됩니다 (메소드가 아닌 경우 static).

BaseClass bcdc = new DerivedClass(); 
bcdc.doSomething() // will invoke DerivedClass's doSomething method.

이제 방법을 이해하는 데 너무 많은 혼란과 하드있을 수 있습니다 C #에 와서 newvirtual+derive또는 가상 + 새로운 재정이 노력하고 있습니다.

나는 왜 세상에서 DerivedClass이름과 서명이 같은 방식으로 내 방법을 추가 BaseClass하고 새로운 행동을 정의 하려고하는지 이해할 수 없지만 런타임 다형성에서 BaseClass메소드가 호출됩니다! (이것은 재정의하는 것이 아니라 논리적으로 이루어져야합니다).

의 경우 virtual + override논리적 구현이 정확하지만 프로그래머가 자신이 코딩시 대체 할 사용자에게 권한을 부여해야하는 방법을 생각해야하지만. 어느 정도 찬반 점이 있습니다 (지금 은 가지 않겠습니다).

왜 C #으로 대한 너무 많은 공간이 있습니다 않은 -logical 추론과 혼란. 그래서 제가 사용을 생각해야하는 현실 세계의 맥락에서 나의 질문을 재구성 할 수있다 virtual + override대신 new도의 사용 new대신 virtual + override?


특히 Omar의 매우 좋은 답변을 얻은 후 C # 디자이너는 프로그래머가 메소드를 만들기 전에 생각해야한다는 것에 대해 더 많은 스트레스를주었습니다. 이는 좋으며 Java의 신인 실수를 처리합니다.

이제 질문이 있습니다. Java에서와 같은 코드가 있다면

Vehicle vehicle = new Car();
vehicle.accelerate();

나중에 나는 SpaceShip에서 파생 된 새 클래스를 만듭니다 Vehicle. 그럼 모두 변경하려면 carA와 SpaceShip코드 한 줄을 변경할 난 그냥이 객체를

Vehicle vehicle = new SpaceShip();
vehicle.accelerate();

이것은 어떤 코드 포인트에서도 내 논리를 깨뜨리지 않습니다.

그러나 C #의 경우 클래스를 SpaceShip재정의하지 않고 사용 하면 내 코드의 논리가 손상됩니다. 단점이 아닌가?Vehicleacceleratenew



답변

C #이 이런 식으로 한 이유를 물었으므로 C # 작성자에게 문의하는 것이 가장 좋습니다. C #의 수석 아키텍트 인 Anders Hejlsberg는 인터뷰 에서 기본적으로 (Java와 같이) 가상으로 가지 않기로 선택한 이유를 다음과 같이 대답했습니다 .

Java는 기본적으로 메소드를 비가 상으로 표시하기 위해 final 키워드 와 함께 가상을가집니다. 여전히 두 가지 개념을 배우지 만 많은 사람들은 최종 키워드에 대해 알지 못하거나 적극적으로 사용하지 않습니다. C #은 가상 및 새 / 재정의를 사용하여 의도적으로 이러한 결정을 내립니다.

몇 가지 이유가 있습니다. 하나는 성능 입니다. 사람들이 Java로 코드를 작성할 때 메소드를 최종으로 표시하는 것을 잊어 버렸습니다. 따라서 이러한 방법은 가상입니다. 그것들은 가상이기 때문에 성능이 좋지 않습니다. 가상 메소드와 관련된 성능 오버 헤드가 있습니다. 그것은 하나의 문제입니다.

더 중요한 문제는 버전 관리 입니다. 가상 방법에 대한 두 가지 생각 학교가 있습니다. 아카데믹 스쿨은 “언젠가 그것을 무시하고 싶을 수 있기 때문에 모든 것이 가상이어야한다”고 말합니다. 실제 환경에서 실행되는 실제 응용 프로그램을 구축하여 나온 실용적인 사고 학교는 “우리는 가상으로 만드는 것에 대해 신중해야합니다.”라고 말합니다.

우리가 플랫폼에서 가상의 것을 만들 때, 우리는 그것이 미래에 어떻게 진화하는지에 대해 엄청나게 많은 약속을합니다. 비가 상 메서드의 경우이 메서드를 호출하면 x와 y가 발생합니다. API에서 가상 메서드를 게시 할 때이 메서드를 호출 할 때 x와 y가 발생한다고 약속 할뿐입니다. 또한이 메소드를 재정의하면 다른 메소드와 관련하여이 특정 순서로 메소드를 호출하고 상태는이 상태와 불변 상태가됩니다.

API에서 virtual을 말할 때마다 콜백 후크가 생성됩니다. OS 또는 API 프레임 워크 디자이너로서 이에 대해주의를 기울여야합니다. 이러한 약속을 반드시 이행 할 수는 없기 때문에 사용자가 API의 임의의 시점에서 사용자를 재정의하고 후킹하는 것을 원하지 않습니다. 그리고 사람들은 무언가를 가상으로 만들 때의 약속을 완전히 이해하지 못할 수도 있습니다.

인터뷰는 개발자가 클래스 상속 디자인에 대해 어떻게 생각하는지와 그것이 어떻게 결정을 이끌어 냈는지에 대해 더 많은 토론을했습니다.

이제 다음 질문으로 넘어갑니다.

나는 왜 세상에서 BaseClass와 동일한 이름과 동일한 서명으로 DerivedClass에 메소드를 추가하고 새로운 동작을 정의 할 것인지 이해할 수 없지만 런타임 다형성에서 BaseClass 메소드가 호출됩니다! (이것은 재정의되지 않고 논리적으로 있어야합니다).

파생 클래스가 기본 클래스의 계약을 준수하지 않지만 이름이 같은 메서드가 있음을 선언하려는 경우입니다. C # newoverrideC # 의 차이점을 모르는 사람 은이 MSDN 페이지를 참조하십시오 .

매우 실용적인 시나리오는 다음과 같습니다.

  • 라는 클래스가있는 API를 만들었습니다 Vehicle.
  • API를 사용하기 시작했고 파생되었습니다 Vehicle.
  • 귀하의 Vehicle클래스는 어떤 방법이 없었다 PerformEngineCheck().
  • 내에서 Car클래스 나 메소드를 추가 PerformEngineCheck().
  • 새 버전의 API를 출시하고를 추가했습니다 PerformEngineCheck().
  • 클라이언트가 API에 의존하기 때문에 메소드의 이름을 바꿀 수 없어서 메소드가 손상 될 수 있습니다.
  • 따라서 새 API를 다시 컴파일하면 C #에서이 문제에 대해 경고합니다.

    베이스 PerformEngineCheck()가 아닌 경우 virtual:

    app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
    Use the new keyword if hiding was intended.
    

    그리고 기지 PerformEngineCheck()virtual:

    app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
    To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
    
  • 이제 클래스가 실제로 기본 클래스의 계약을 확장하는지 아니면 다른 계약이지만 동일한 이름인지 여부를 명시 적으로 결정해야합니다.

  • 를함으로써 new기본 방법의 기능이 파생 된 방법과 다른 있었다면, 나는 내 고객을 아프게하지 않습니다. 참조 된 코드는 호출 Vehicle되지 않지만 참조 Car.PerformEngineCheck()가있는 코드 Car는에서 제공 한 것과 동일한 기능을 계속 볼 수 PerformEngineCheck()있습니다.

비슷한 예는 기본 클래스의 다른 메소드가 호출 할 수 PerformEngineCheck()있는 경우 (예 : 최신 버전) PerformEngineCheck()파생 클래스의 메소드 를 호출하지 못하게하는 방법은 무엇입니까? Java에서는 해당 결정이 기본 클래스와 관련이 있지만 파생 클래스에 대해서는 아무것도 모릅니다. C #에서, 그 결정이 달려 모두 (비아 기본 클래스에 대한 virtual키워드를) 파생 클래스 (바이어 newoverride키워드).

물론, 컴파일러에서 발생하는 오류는 프로그래머가 예기치 않게 오류를 발생시키지 않도록하는 유용한 도구를 제공합니다 (예 :이를 무시하지 않고 새로운 기능을 무시하거나 제공함).

Anders가 말했듯이, 현실 세계는 우리를 처음부터 시작한다면 결코 들어가고 싶지 않은 그런 문제로 우리를 강요합니다.

편집 : new인터페이스 호환성을 보장하기 위해 사용해야하는 위치에 대한 예가 추가되었습니다 .

편집 : 의견을 검토하는 동안 Eric Lippert (C # 디자인위원회 회원 중 하나)가 다른 예제 시나리오 (Brian이 언급 한)에 대한 글을 읽었습니다.


2 부 : 업데이트 된 질문에 근거

그러나 C #의 경우 SpaceShip이 Vehicle 클래스의 가속을 재정의하지 않고 new를 사용하면 코드의 논리가 손상됩니다. 단점이 아닌가?

누가 SpaceShip실제로 재정의 할지 아니면 다른지를 결정 Vehicle.accelerate()합니까? SpaceShip개발자 여야합니다 . 따라서 SpaceShip개발자가 기본 클래스의 계약을 유지하지 않기로 결정한 경우으로 전화를 Vehicle.accelerate()걸어야 SpaceShip.accelerate()합니까? 그때는로 표시됩니다 new. 그러나 계약을 실제로 유지하기로 결정한 경우 실제로 계약을 표시합니다 override. 두 경우 모두 계약 에 따라 올바른 메소드를 호출하여 코드가 올바르게 작동합니다 . 코드 SpaceShip.accelerate()가 실제로 재정의 하는지 Vehicle.accelerate()또는 이름 충돌 인지 어떻게 결정할 수 있습니까? (위의 예를 참조하십시오).

그러나 암시 적 상속의 경우 계약SpaceShip.accelerate()유지하지 않더라도 메소드 호출은 여전히로 이동합니다 .Vehicle.accelerate()SpaceShip.accelerate()


답변

올바른 일이기 때문에 완료되었습니다. 사실 모든 메소드를 재정의하는 것이 잘못되었습니다. 기본 클래스에 대한 변경으로 인해 서브 클래스가 중단되는지 알 수있는 취약한 기본 클래스 문제점이 발생합니다. 따라서 재정의해서는 안되는 메소드를 블랙리스트에 올리거나 재정의 할 수있는 메소드를 화이트리스트에 추가해야합니다. 둘 중 화이트리스트는 더 안전 할뿐만 아니라 ( 실수로 취약한 기본 클래스를 만들 수 없기 때문에 ) 작곡을 선호하는 상속을 피해야하기 때문에 더 적은 작업이 필요합니다.


답변

Robert Harvey가 말했듯이 모든 것이 익숙합니다. Java 에는 이러한 유연성이 부족하다는 것이 이상합니다.

즉, 왜 이것이 처음에 있습니까? C 번호가 가지고있는 같은 이유를 들어 public, internal(또한 “아무것도”) protected, protected internal그리고 private하지만, 자바 그냥이 public, protected아무것도하고 private. 추적 할 용어와 키워드가 더 많아지면서 코딩 대상의 동작을보다 세밀하게 제어 할 수 있습니다.

newvs 의 경우 virtual+override다음과 같습니다.

  • 당신이 방법을 구현하는 서브 클래스를 강제하려는 경우, 사용 abstract하고, override하위 클래스이다.
  • 당신이 기능을 제공하지만, 서브 클래스를 대체 할 수 있도록하려면, 사용 virtual, 및 override서브 클래스이다.
  • 서브 클래스가 무시할 필요가없는 기능을 제공하려면 아무 것도 사용하지 마십시오.
    • 그런 다음 특별한 경우 하위 클래스가 있으면 않는 다르게 행동 할 필요를 사용하는 new하위 클래스에서.
    • 더 서브 클래스 수 있도록하려면 지금 행동을 무시하지, 사용하는 sealed기본 클래스에서.

실제 사례 : 다양한 소스에서 처리 한 전자 상거래 주문을 처리하는 프로젝트. 각 소스의 자식 클래스가 재정의 OrderProcessor할 특정 abstract/ virtual메서드 와 함께 대부분의 논리를 가진 기반 이있었습니다 . 주문을 처리하는 방법이 완전히 다른 새로운 소스를 얻을 때까지 핵심 기능을 대체해야 할 때까지 잘 작동했습니다 . 이 시점에서 우리는 두 가지 선택을 가졌다 : 1) virtual기본 방법과 override자식에 추가; 또는 2) new아이에게 추가하십시오 .

어느 쪽이든 작동 할 수 있지만 , 첫 번째 방법은 나중에 특정 방법을 다시 무시하기가 매우 쉽습니다. 예를 들어 자동 완성으로 표시됩니다. 그러나 이것은 예외적 인 경우 였으므로 new대신 사용하기로 결정했습니다 . “이 방법은 재정의 할 필요가 없습니다”라는 표준을 유지하면서 특별한 경우를 허용했습니다. 인생을 더 쉽게 만드는 의미상의 차이입니다.

그러나 의미상의 차이 만이 아니라 이와 관련된 동작 차이 있음에 유의하십시오 . 자세한 내용은 이 기사 를 참조하십시오. 그러나 나는이 행동을 활용 해야하는 상황에 처한 적이 없습니다.


답변

Java의 설계는 객체에 대한 참조가 주어지면 특정 매개 변수 유형을 사용하여 특정 메소드 이름을 호출하면 항상 동일한 메소드를 호출합니다. 암시 적 매개 변수 유형 변환은 참조 유형에 의해 영향을받을 수 있지만 이러한 변환이 모두 해결되면 참조 유형은 관련이 없습니다.

이렇게하면 런타임이 간단 해지지 만 불행한 문제가 발생할 수 있습니다. 가정 GrafBase구현하지 않고 void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3)있지만, GrafDerived계산 된 제 포인트 제 대향하는 평행 무 공용 방법으로 구현한다. 이후 버전의 버전은 GrafBase동일한 서명으로 공용 메소드를 구현하지만 계산 된 네 번째 지점은 두 번째 지점과 반대라고 가정하십시오. 수신 GrafBase하지만 고객에 대한 참조를받는 고객 은 새 방법 의 방식으로 네 번째 포인트를 계산할 GrafDerived것으로 예상 되지만 기본 방법을 변경하기 전에 사용한 고객은 원래 구현 된 동작을 예상합니다 .DrawParallelogramGrafBaseGrafDerived.DrawParallelogramGrafDerived

Java에서는 작성자 GrafDerived가 클래스 를 정의 하기 전에 사용한 기존 클라이언트 코드와의 호환성을 유지 하면서 새 GrafBase.DrawParallelogram메소드 를 사용하는 클라이언트와 공존 할 수있는 방법 GrafDerived이 없습니다 . (가)부터 클라이언트의 종류를 호출하는 것을 말할 수 없다 클라이언트 코드의 종류에 의해 호출 될 때, 그것은 동일하게 작동해야합니다. 두 종류의 클라이언트 코드는 어떻게 동작해야하는지에 대한 기대치가 다르기 때문에 그 중 하나에 대한 합법적 인 기대치를 위반하는 것을 피할 수있는 방법 은 없습니다 (예 : 합법적 인 클라이언트 코드 위반).GrafDerived.DrawParallelogramGrafBaseDrawParallelogramGrafDerived

C #에서 GrafDerived다시 컴파일되지 않으면 런타임은 DrawParallelogram형식 참조에 대해 메서드를 호출하는 코드가 마지막으로 컴파일되었을 때 GrafDerived의 동작 GrafDerived.DrawParallelogram()을 예상 한다고 가정 하지만 형식 참조에 대해 메서드를 호출하는 코드 GrafBase는 예상 할 것 GrafBase.DrawParallelogram( 추가되었습니다). 경우 GrafDerived나중에 향상된의 존재를 다시 컴파일 GrafBase컴파일러 프로그래머까지 꽥꽥 어느 그의 방법은 유전 부재에 대한 올바른 대체하도록 의도 된 것인지 여부를 지정한다 GrafBase, 또는 그 동작 유형의 참조에 연결되어야하는지 여부 GrafDerived가 아니라해야 type 참조의 동작을 대체하십시오 GrafBase.

서명과 동일한 서명 GrafDerived을 가진 구성원과 다른 작업 을 수행 하는 방법 GrafBase이 있으면 디자인이 잘못되었음을 나타내므로 지원하지 않아야 한다고 합리적으로 주장 할 수 있습니다. 불행하게도, 기본 유형의 작성자는 파생 된 유형에 추가 될 수있는 방법을 알 수있는 방법이 없으며 그 반대도 마찬가지이므로 기본 클래스와 파생 클래스 클라이언트가 유사한 이름의 메소드에 대해 다른 기대를 갖는 상황은 본질적으로 피할 수 없습니다 다른 사람이 추가 할 수있는 이름을 아무도 추가 할 수 없습니다. 문제는 그러한 이름 복제가 발생해야하는지가 아니라 발생할 때 피해를 최소화하는 방법입니다.


답변

표준 상황 :

여러 프로젝트에서 사용하는 기본 클래스의 소유자입니다. 당신은 실제 가치를 제공하는 프로젝트에서 1과 무수한 파생 클래스 사이에서 나올 기본 클래스를 변경하려고합니다 (프레임 워크는 한 번의 제거만으로 가치를 제공하며 실제 인간은 프레임 워크를 원하지 않습니다) 프레임 워크에서 실행되는 것). 파생 클래스의 바쁜 소유자에게 알리는 행운을 빕니다. “결정을 승인해야하는 사람들에게”프레임 워크 : 프로젝트의 계층화와 버그의 원인 “이라는 응답을 얻지 않고”변경해야합니다.이 방법을 재정의해서는 안됩니다. “

특히 재정의 불가능하다고 선언하지 않으면 변경을 방지하는 작업을 수행해도 좋다고 암시 적으로 선언했습니다.

그리고 기본 클래스를 재정 의하여 실제 가치를 제공하는 파생 클래스가 많지 않은 경우 왜 기본 클래스입니까? 희망은 강력한 동기 부 여자이지만 참조되지 않은 코드로 끝나는 매우 좋은 방법입니다.

최종 결과 : 프레임 워크 기본 클래스 코드는 엄청나게 깨지기 쉬우 며 정적 인 상태가되어 현재 / 효율을 유지하기 위해 필요한 변경을 수행 할 수 없습니다. 또는 프레임 워크가 불안정성 (유래 클래스가 계속 깨짐)에 대한 응답을 얻었고 프레임 워크를 사용하는 주된 이유는 코딩을 더 빠르고 안정적으로 만들기 때문에 사람들이 전혀 사용하지 않을 것입니다.

간단히 말해서, 바쁜 프로젝트 소유자에게 당신이 소개하고있는 버그를 수정하기 위해 프로젝트를 지연 시키도록 요구할 수 없으며, 원래의 “결함”이 있더라도 버그를 크게 없애지 않는 한 “멀리 나가기”보다 더 나은 것을 기대할 수 없습니다. 그것들은 논쟁의 여지가 많았습니다.

처음에는 “비 가상적”이 아닌 곳에서 잘못된 일을하지 않도록하는 것이 좋습니다. 누군가가이 특별한 방법을 재정의 할 수있는 이유가 분명한 이유가있는 이유는 무엇입니까? 안전해야합니다 . 다른 사람의 코드를 손상 시키지 않고 “잠금 해제”할 수 있습니다 .


답변

비가 상으로 기본 설정하는 것은 기본 클래스 개발자가 완벽하다고 가정합니다. 내 경험상 개발자는 완벽하지 않습니다. 기본 클래스 개발자가 메서드를 재정의하거나 가상 추가를 잊어 버릴 수있는 사용 사례를 상상할 수없는 경우 기본 클래스를 수정하지 않고 기본 클래스를 확장 할 때 다형성을 활용할 수 없습니다. 현실에서 기본 클래스를 수정하는 것은 종종 옵션이 아닙니다.

C #에서 기본 클래스 개발자는 하위 클래스 개발자를 신뢰하지 않습니다. Java에서 서브 클래스 개발자는 기본 클래스 개발자를 신뢰하지 않습니다. 서브 클래스 개발자는 서브 클래스에 대한 책임이 있으며 기본 클래스를 확장 할 수있는 권한을 부여해야합니다 (명시 적 거부는 제외하고 Java에서는이 오류가 발생할 수도 있음).

언어 정의의 기본 속성입니다. 그것은 옳고 그른 것이 아니며, 그것이 무엇이고 바꿀 수없는 것입니다.