c# List <T>로부터 상속받지 않는 이유는 무엇입니까?





13 Answers

마지막으로, 어떤 것은 List를 wrapping하는 것을 제안한다.

그것이 올바른 방법입니다. "불필요하게 말씨"는 이것을 보는 나쁜 방법입니다. my_team.Players.Count 를 작성할 때 명시 적으로 의미가 있습니다. 당신은 선수들을 세고 싶습니다.

my_team.Count

..아무 의미 없다. 뭐라구?

팀은 단순한 선수 목록 이상으로 구성된 목록이 아닙니다. 팀이 플레이어를 소유하므로 플레이어가 멤버가되어야합니다 (멤버).

지나치게 자세한 내용 걱정된다면 항상 팀의 속성을 노출 할 수 있습니다.

public int PlayerCount {
    get {
        return Players.Count;
    }
}

..된다 :

my_team.PlayerCount

이것은 지금 의미가 있으며 Demeter의 법칙을 준수합니다.

복합 재사용 원칙을 고수하는 것도 고려해야합니다. List<T> 를 상속하면 팀 플레이어리스트이며 불필요한 메소드를 노출한다고 말할 수 있습니다. 이것은 틀린 말입니다 - 명시된 바와 같이 팀은 이름, 관리자, 임원, 강사, 의료진, 급여 한도 등을 가진 선수 목록 이상입니다. 팀 수업에 선수 목록이 포함되어 있으면 "팀 에는 선수 명단 있다"고 말하고 있지만 다른 것들도 가질 수있다.

c# .net list oop inheritance

내 프로그램을 계획 할 때 종종 다음과 같은 사슬로 시작합니다.

축구 팀은 축구 선수 목록에 불과합니다. 그러므로 나는 다음과 같이 표현해야한다.

var football_team = new List<FootballPlayer>();

이 목록의 순서는 플레이어가 명부에 올라있는 순서를 나타냅니다.

그러나 나중에 팀은 기록되어야하는 단순한 선수 명단 외에 다른 속성을 가지고 있음을 깨닫습니다. 예를 들어, 이번 시즌의 총 득점, 현재 예산, 유니폼 색상, 팀 이름을 나타내는 string 등.

그래서 나는 생각한다.

좋아요, 풋볼 팀은 선수 목록과 비슷하지만, 이름 ( string )과 누적 합계 ( int )가 있습니다. .NET은 축구 팀 저장을위한 클래스를 제공하지 않으므로 내 자신의 클래스를 만들 것입니다. 가장 유사하고 관련성이 높은 기존 구조는 List<FootballPlayer> 이므로이를 상속받습니다.

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

그러나 List<T> 에서 상속해서는 안된다는 지침 이 있음이 밝혀졌습니다. 나는 두 가지면에서이 가이드 라인에 철저히 혼란 스럽다.

왜 안돼?

외관상으로는 List 는 어떻게해서든지 성과를 위해 낙관된다 . 어떻게 그렇게? List 확장하면 어떤 성능 문제가 발생합니까? 무엇이 깰 것인가?

필자가 보았던 또 다른 이유는 List 가 Microsoft에 의해 제공되고,이를 제어 할 수 없기 때문에 "공개 API"를 공개 한 후에 나중에 변경할 수 없다는 것 입니다. 그러나 나는 이것을 이해하기 위해 애 쓰고있다. 공개 API 란 무엇이며 왜 신경 써야합니까? 현재 프로젝트에 공개 API가 없거나없는 경우이 가이드 라인을 무시해도됩니까? List 에서 상속을 받고 공개 API가 필요하다면 어떤 어려움이 있습니까?

그것은 왜 중요합니까? 목록은 목록입니다. 무엇이 바뀔 수 있습니까? 무엇을 바꾸고 싶습니까?

그리고 마지막으로 Microsoft가 List 에서 상속 받기를 원하지 않는다면 왜 클래스를 sealed 하지 않았습니까?

내가 뭘 더 사용해야하니?

분명히 사용자 지정 컬렉션의 경우 Microsoft는 List 대신 확장해야하는 Collection 클래스를 제공합니다. 그러나이 클래스는 매우 베어 AddRange 와 같은 많은 유용한 것들을 가지고 있지 않습니다. jvitor83의 대답 은 특정 메소드에 대한 성능 근거를 제공하지만, 느린 AddRange 아닌 것보다 나은 점은 무엇입니까?

Collection 에서 상속하는 것은 List 에서 상속하는 것보다 더 많은 작업이며 아무런 이점도 없습니다. 확실히 Microsoft는 아무런 이유없이 추가 작업을 수행하라고 말하지 않을 것입니다. 그래서 나는 어떻게 든 오해하고있는 것처럼 느껴질 수밖에 없으며 Collection 상속하는 것은 실제로 내 문제에 대한 올바른 해결책이 아닙니다.

IList 구현과 같은 제안을 보았습니다. 그냥 싫다. 이것은 아무것도 얻지 못하는 상용구 코드 수십 라인입니다.

마지막으로, 어떤 것은 List 를 wrapping하는 것을 제안한다.

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

이 문제에는 두 가지 문제가 있습니다.

  1. 그것은 내 코드를 불필요하게 장황하게 만든다. 이제 my_team.Players.Count 대신 my_team.Players.Count 를 호출해야합니다. 고맙게도 C #을 사용하면 색인 생성기를 정의하여 색인 생성을 투명하게 만들 수 있으며 내부 List 의 모든 메서드를 전달할 수 있습니다. 나는 그 모든 일을 위해 무엇을 얻습니까?

  2. 그것은 평범하지 않습니다. 축구 팀은 선수 명단을 갖고 있지 않습니다. 그것은 선수 목록입니다. 당신은 "John McFootballer가 SomeTeam의 선수들과 합류했다고"말하지 않습니다. 당신은 "존이 SomeTeam에 가입했다"고 말합니다. "문자열의 문자"에 문자를 추가하지 않으면 문자열에 문자를 추가합니다. 도서관의 서적에 책을 추가하지 않고 서적을 도서관에 추가합니다.

"내부에서 일어나는 일"이 "Y를 내부 목록에 추가"한다고 말할 수 있지만 이것은 세계에 대해 매우 반 직관적 인 사고 방식처럼 보입니다.

내 질문 (요약)

"논리적으로"(즉, "인간의 마음에") 몇 가지 종소리와 호루라기가있는 list things 뿐인 데이터 구조를 나타내는 올바른 C # 방식은 무엇입니까?

List<T> 로부터 상속받을 수 없는가? 언제 받아 들일 수 있습니까? 왜 안돼? List<T> 로부터 상속받을 것인지 아닌지를 결정할 때 프로그래머는 무엇을 고려해야 하는가?




class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

이전 코드는 거리에서 축구를하는 사람들이 많다는 것을 의미합니다. 같은 것 :

어쨌든,이 코드는 (내 답변에서)

public class FootballTeam
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
    get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

의미 : 이것은 관리, 선수, 관리자 등이있는 축구 팀입니다. 다음과 같은 것 :

이것은 당신의 논리가 그림에 어떻게 표시되는지입니다 ...




모두가 지적했듯이, 한 팀의 팀은 선수 명단이 아닙니다. 이 실수는 다양한 분야의 전문가에 의해 이루어집니다. 종종이 문제는 미묘하고 때때로 매우 심합니다. 이러한 디자인은 Liskov Substitution Principle을 위반하기 때문에 좋지 않습니다. 인터넷에는 http://en.wikipedia.org/wiki/Liskov_substitution_principle 이 개념을 설명하는 좋은 기사가 많이 있습니다 http://en.wikipedia.org/wiki/Liskov_substitution_principle

요약하면 클래스 간의 부모 / 자식 관계에는 다음 두 가지 규칙이 유지됩니다.

  • 자녀는 부모를 완전히 정의하는 것보다 적은 특성을 요구해서는 안됩니다.
  • 부모는 아동을 완전히 정의하는 것 이외에 어떤 특성도 요구해서는 안됩니다.

즉, 학부모는 아동에 대해 필요한 정의이며, 아동은 학부모에 대한 충분한 정의입니다.

여기에 하나의 해결책을 생각해보고 그러한 실수를 피하는 데 도움이되는 위의 원칙을 적용하는 방법이 있습니다. 부모 클래스의 모든 연산이 구조적 및 의미 론적으로 파생 클래스에 대해 유효한지 검증함으로써 가설을 테스트해야합니다.

  • 축구 팀이 축구 선수 명단인가요? (목록의 모든 속성은 동일한 의미의 팀에 적용됩니다)
    • 팀은 동질적인 단체입니까? 예, 팀은 Players 컬렉션입니다.
    • 플레이어의 포함 순서는 팀의 상태를 설명하고 명시 적으로 변경하지 않으면 시퀀스가 ​​유지되는지 팀이 확인합니까? 아니요, 아니요.
    • 선수들은 팀의 연속적인 위치에 따라 포함 / 드롭 될 것으로 예상됩니까? 아니

보시다시피 목록의 첫 번째 특성 만 팀에 적용됩니다. 따라서 팀은 목록이 아닙니다. 목록은 팀 관리 방법에 대한 구현 세부 사항이므로 플레이어 개체를 저장하고 Team 클래스의 메서드로 조작해야합니다.

이 시점에서 Team 클래스는 List를 사용하여 구현되지 않아야한다고 생각합니다. 대부분의 경우 Set 데이터 구조 (예 : HashSet)를 사용하여 구현해야합니다.




우선, 그것은 유용성과 관련이 있습니다. 상속을 사용하는 경우 Team 클래스는 순수하게 객체 조작을 위해 설계된 비헤이비어 (메서드)를 노출합니다. 예를 들어 AsReadOnly() 또는 CopyTo(obj) 메서드는 팀 객체에 대해 아무런 의미가 없습니다. AddRange(items) 메서드 대신에 당신은 아마 더 AddPlayers(players) 메서드를 원할 것입니다.

LINQ를 사용하려면 ICollection<T> 또는 IEnumerable<T> 과 같은 일반 인터페이스를 구현하는 것이 더 적합합니다.

앞서 언급했듯이 구성이 올바른 방법입니다. 플레이어 목록을 개인 변수로 구현하면됩니다.




축구 팀 축구 선수 목록 이 아닙니다 . 축구 팀 축구 선수 목록으로 구성되어 있습니다 !

이것은 논리적으로 잘못되었습니다.

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

이 말이 맞습니다.

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}



질문을 다시 써 보겠습니다. 그래서 다른 관점에서 주제를 볼 수 있습니다.

축구 팀을 대표해야 할 때 기본적으로 이름이라는 것을 이해합니다. Like : "The Eagles"

string team = new string();

그런 다음 나중에 팀에 선수가 있다는 것을 알게되었습니다.

왜 나는 문자열 유형을 확장하여 플레이어 목록도 보유 할 수 없습니까?

문제에 대한 귀하의 입장은 임의적입니다. 팀이 무엇을 생각하는 시도 그렇지 않은 것 (특성) 입니다 .

그런 다음 다른 클래스와 속성을 공유하는지 확인할 수 있습니다. 상속에 대해서 생각해보십시오.




사람들이 말하도록 허용합니까?

myTeam.subList(3, 5);

전혀 이해가 되니? 그렇지 않은 경우 List가 아니어야합니다.




여기에 많은 훌륭한 해답이 있지만 언급하지 않은 것을 만지고 싶습니다. 객체 지향 설계는 객체힘을 실어주는 것입니다 .

모든 규칙, 추가 작업 및 내부 세부 정보를 적절한 개체에 캡슐화하려고합니다. 이런 방식으로이 객체와 상호 작용하는 다른 객체는 모두 그것에 대해 걱정할 필요가 없습니다. 실제로 한 단계 더 나아가 다른 객체가 이러한 내부 객체를 우회하는 것을 적극적으로 방지 하고자합니다 .

상속 받으면 List다른 모든 개체가 사용자를 목록으로 볼 수 있습니다. 플레이어 추가 및 제거 방법에 직접 액세스 할 수 있습니다. 그리고 당신은 당신의 통제를 잃었을 것입니다. 예 :

플레이어가 퇴직했는지, 사임했는지, 해고되었는지 여부를 알면 플레이어가 퇴장 할 때를 차별화하려고한다고 가정합니다. RemovePlayer적절한 입력 enum을 취하는 메소드를 구현할 수 있습니다. 그러나 상속에 의해 List, 당신은 직접 액세스 방지 할 수없는 것 Remove, RemoveAll심지어는과 Clear. 결과적으로, 당신은 실제로 당신의 수업을 거부했습니다FootballTeam .

캡슐화에 대한 추가 고려 사항 ... 다음과 같은 우려 사항을 제기했습니다.

그것은 내 코드를 불필요하게 장황하게 만든다. 이제 my_team.Count 대신 my_team.Players.Count를 호출해야합니다.

모든 클라이언트가 팀을 사용하는 데 불필요하게 자세한 정보가 표시됩니다. 그러나 그 문제는 당신이 List Players모든 것과 잡다한 것에 노출 되어 있기 때문에 당신의 동의없이 당신 팀을 도울 수 있다는 사실에 비해 매우 적습니다 .

당신은 말하기를 계속합니다 :

그것은 평범하지 않습니다. 축구 팀은 선수 명단을 갖고 있지 않습니다. 그것은 선수 목록입니다. 당신은 "John McFootballer가 SomeTeam의 선수들과 합류했다고"말하지 않습니다. 당신은 "존이 SomeTeam에 가입했다"고 말합니다.

첫 번째 비트에 대해 틀렸어. '목록'이라는 단어를 버리면 실제로 팀에 선수가 있다는 것이 분명합니다.
그러나 두 번째로 머리에 못을 박았습니다. 고객이 전화하는 것을 원하지 않습니다 ateam.Players.Add(...). 너는 그들에게 전화하기를 원한다 ateam.AddPlayer(...). 그리고 당신의 구현은 (아마도 다른 것들 사이에서) Players.Add(...)내부적으로 호출 할 것 입니다.

캡슐화가 객체에 권한을 부여하는 목적에 얼마나 중요한지 알 수 있기를 바랍니다. 당신은 다른 객체로부터의 간섭을 두려워하지 않고 각 클래스가 잘할 수 있도록하고 싶습니다.




이것은 나에게 "있는 것"대 "있는 것"을 생각 나게합니다. 때로는 수퍼 클래스에서 직접 상속하는 것이 더 쉽고 더 중요합니다. 다른 경우 독립형 클래스를 만들고 멤버 변수에서 상속 한 클래스를 포함시키는 것이 더 중요합니다. 클래스의 기능에 계속 액세스 할 수는 있지만 인터페이스 나 클래스에서 상속 할 때 생길 수있는 다른 제약 사항에 바인딩되지는 않습니다.

어느 쪽을합니까? 많은 것들과 마찬가지로 ... 그것은 상황에 달려 있습니다. 내가 사용할 가이드는 다른 클래스에서 상속 받기 위해서는 진정으로 "is a"관계 여야한다는 것입니다. 따라서 BMW라는 클래스를 작성하면 BMW가 실제로 차이기 때문에 Car에서 상속받을 수 있습니다. Horse 클래스는 말에서 실제로 포유 동물이며 실제 포유 동물의 기능은 Horse와 관련이 있어야하기 때문에 Mammal 클래스에서 상속받을 수 있습니다. 하지만 팀이 목록이라고 말할 수 있습니까? 내가 말할 수있는 것에서 볼 때 팀과 같은 것은 실제로 "목록"입니다. 그래서이 경우 멤버 변수로 List가 있습니다.




지침에 따르면 공용 API는 목록, 세트, ​​사전, 트리 등을 사용하는지 여부에 대한 내부 설계 결정을 공개해서는 안됩니다. "팀"은 반드시 목록 일 필요는 없습니다. 목록으로 구현할 수도 있지만 공개 API 사용자는 기초 지식을 바탕으로 수업을 사용해야합니다. 이를 통해 의사 결정을 변경하고 공용 인터페이스에 영향을주지 않고 다른 데이터 구조를 사용할 수 있습니다.




대부분의 답변으로는 복잡한 비교가 없지만 이러한 상황을 처리하기위한 방법을 공유하고 싶습니다. 확장을 IEnumerable<T>통해 Team클래스가 모든 메소드와 속성을 공개하지 않고도 Linq 쿼리 확장을 지원할 수 있습니다 List<T>.

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}



나는 에펠의 발명가이자 계약에 의한 디자인 인 버트 랜드 마이어 (Bertrand Meyer) 가 눈꺼풀을 때리는 것없이 Team상속 받았을 것이라고 List<Player>덧붙였다.

그의 책 Object-Oriented Software Construction 에서 사각형 윈도우가 자식 윈도우를 가질 수있는 GUI 시스템의 구현에 대해 설명합니다. 그는 단순히 한 Window모두 상속 RectangleTree<Window>구현을 재사용 할 수 있습니다.

그러나 C #은 Eiffel이 아닙니다. 후자는 다중 상속과 기능의 이름 변경을 지원 합니다 . C #에서는 하위 클래스를 만들 때 인터페이스와 구현을 모두 상속받습니다. 구현을 재정의 할 수 있지만 호출 규칙은 수퍼 클래스에서 직접 복사됩니다. 그러나 Eiffel에서는 공용 메서드의 이름을 수정할 수 있으므로 이름을 바꿀 수 Add있고 Remove에서 HireFire로 이름을 바꿀 수 있습니다 Team. 인스턴스 Team가 다시 업 캐스트 List<Player>되면 호출자는이를 사용 Add하고 Remove수정하지만 가상 메소드 HireFire호출됩니다.




인터페이스보다 클래스 선호

클래스는 클래스로부터 파생되는 것을 피하고 필요한 최소한의 인터페이스를 구현해야합니다.

상속으로 캡슐화가 끊어짐

클래스에서 파생 되면 캡슐화가 중단됩니다 .

  • 컬렉션 구현 방법에 대한 내부 세부 정보를 제공합니다.
  • 적절하지 않을 수도있는 인터페이스 (공용 함수 및 속성 집합)를 선언합니다.

무엇보다도 이것은 당신의 코드를 리팩토링하는 것을 더 어렵게 만듭니다.

클래스는 구현 세부 사항입니다.

클래스는 코드의 다른 부분에서 숨겨져 야하는 구현 세부 사항입니다. 즉 System.List, 현재와 미래에는 적절할 수도 있고 그렇지 않을 수도있는 추상 데이터 유형의 특정 구현입니다.

개념적으로 System.List데이터 유형을 "목록"이라고 부르는 것은 약간의 붉은 청어이다. A System.List<T>는 요소 추가, 삽입 W 제거에 대한 상각 된 O (1) 조작을 지원하고 요소 수를 검색하거나 색인으로 요소를 가져오고 설정하는 O (1) 조작을 지원하는 변경 가능한 순서 콜렉션입니다.

인터페이스가 작을수록 더 유연합니다.

데이터 구조를 설계 할 때 인터페이스가 간단할수록 코드가 유연 해집니다. LINQ가 얼마나 강력한 지에 대한 데모를 살펴보십시오.

인터페이스 선택 방법

"목록"을 생각할 때 자신에게 "야구 선수 컬렉션을 대표해야합니다"라고 말함으로써 시작해야합니다. 따라서 이것을 클래스로 모델링하기로 결정했다고 가정 해 봅시다. 먼저해야 할 일은이 클래스가 노출해야하는 최소한의 인터페이스 양을 결정하는 것입니다.

이 프로세스를 안내하는 데 도움이되는 몇 가지 질문은 다음과 같습니다.

  • 내가 계산해야하나요? 구현하지 않는 경우IEnumerable<T>
  • 이 컬렉션은 초기화 된 후에 변경 될 예정입니까? 고려하지 않으면 IReadonlyList<T>.
  • 색인으로 항목에 액세스 할 수 있습니까? 중히 여기다ICollection<T>
  • 컬렉션에 항목을 추가하는 순서가 중요합니까? 어쩌면 ISet<T>?
  • 당신이 정말로 이러한 일을 원한다면 앞으로 나아가서 구현하십시오 IList<T>.

이렇게하면 코드의 다른 부분을 야구 선수 컬렉션의 구현 세부 정보에 연결하지 않고 인터페이스를 존중하는 한 구현 방법을 자유롭게 변경할 수 있습니다.

이 접근 방식을 사용하면 코드를 읽고, 리팩터링하고, 재사용하기가 더 쉬워집니다.

상용어 방지에 대한 참고 사항

현대 IDE에서 인터페이스를 구현하는 것은 쉽습니다. 마우스 오른쪽 버튼을 클릭하고 "인터페이스 구현"을 선택하십시오. 필요한 경우 모든 구현을 멤버 클래스로 전달합니다.

즉, 많은 상용구를 작성하는 경우 잠재적으로 더 많은 기능을 노출해야하기 때문입니다. 클래스에서 상속하지 않아야하는 것과 같은 이유입니다.

응용 프로그램에 적합한 더 작은 인터페이스를 설계 할 수 있으며, 필요할 때 다른 인터페이스로 매핑 할 수있는 몇 가지 도우미 확장 기능을 설계 할 수도 있습니다. 이것은 LinqArray 라이브러리에IArray 대한 자신의 인터페이스 에서 취한 접근법 입니다.




Related