제네릭과 일반적인 인터페이스?


20

지난번에 일반 수업을 썼을 때 기억이 나지 않습니다. 내가 생각한 후에 생각이들 때마다 나는 그렇지 않다는 결론을 내린다.

이 질문에 대한 두 번째 대답 은 설명을 요구하게 만들었습니다 (아직 말할 수 없기 때문에 새로운 질문을했습니다).

제네릭이 필요한 경우를 예로 들어 주어진 코드를 보자.

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

형식 제약 조건이 있습니다. IBusinessObject

나의 일반적인 생각은 : 클래스는 사용이 제한되어 IBusinessObject있으므로 이것을 사용하는 클래스도 마찬가지 Repository입니다. 리포지토리에는 이러한 것들이 저장되어 있으며 IBusinessObject, 대부분의 클라이언트는 인터페이스를 Repository통해 객체를 가져오고 사용하기를 원할 것 IBusinessObject입니다. 그래서 왜

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

이 예제는 단지 또 다른 컬렉션 유형이고 일반 컬렉션은 클래식이기 때문에 좋지 않습니다. 이 경우 형식 제약 조건도 이상하게 보입니다.

실제로이 예 class Repository<T> where T : class, IBusinessbBject는 나와 거의 비슷해 보입니다 class BusinessObjectRepository. 제네릭이 고쳐야 할 것은 무엇입니까?

요점은 다음과 같습니다. 제네릭은 컬렉션을 제외한 모든 것에 유용하며 클래스 내에서 제네릭 형식 매개 변수 대신이 유형 제약 조건을 사용하면 형식 제약 조건이 특수화 된 것으로 일반화하지 않습니까?

답변:


24

먼저 순수한 파라 메트릭 다형성에 대해 이야기하고 나중에 경계 다형성에 들어 갑시다.

파라 메트릭 다형성

파라 메트릭 다형성이란 무엇입니까? 글쎄, 그것은 유형 또는 오히려 유형 생성자가 유형에 의해 매개 변수화 된다는 것을 의미합니다 . 유형이 매개 변수로 전달되므로 유형을 미리 알 수 없습니다. 이를 바탕으로 어떤 가정도 할 수 없습니다. 자, 그것이 무엇인지 모른다면, 용도는 무엇입니까? 당신은 그것으로 무엇을 할 수 있습니까?

예를 들어, 저장하고 검색 할 수 있습니다. 당신이 이미 언급 한 경우입니다 : 컬렉션. 목록이나 배열에 항목을 저장하려면 항목에 대해 아무것도 알 필요가 없습니다. 목록이나 배열은 유형을 완전히 알 수 없습니다.

그러나 Maybe유형은 어떻습니까? 익숙 Maybe하지 않은 경우 값이 있고 그렇지 않은 유형입니다. 어디서 사용하겠습니까? 예를 들어, 사전에서 항목을 가져올 때 : 항목이 사전에 없을 수 있다는 사실은 예외적 인 상황이 아니므로 항목이 없으면 예외를 발생시키지 않아야합니다. 대신, 당신의 하위 유형의 인스턴스를 반환 Maybe<T>정확히 두 개의 하위 유형이있다 : NoneSome<T>. 예외 또는 전체 춤 을 던지는 대신 int.Parse실제로 반환해야 할 무언가의 또 다른 후보입니다 .Maybe<int>int.TryParse(out bla)

자, 당신 Maybe은 0 또는 하나의 요소 만 가질 수있는 목록과 같은 일종의 소르 타라고 주장 할 수 있습니다 . 따라서 소장품 모음입니다.

그럼 Task<T>어때요? 미래의 어느 시점에서 값을 반환 할 것을 약속하지만 현재 가치가있는 것은 아닙니다.

아니면 어떻 Func<T, …>습니까? 유형에 대해 추상화 할 수없는 경우 한 유형에서 다른 유형으로 함수의 개념을 어떻게 표현 하시겠습니까?

또는보다 일반적으로 추상화와 재사용이 소프트웨어 엔지니어링 의 두 가지 기본 작업 이라는 점을 고려할 때 유형에 대해 추상화 할 수 없는 이유 무엇입니까?

경계 다형성

이제 경계 다형성에 대해 이야기 해 봅시다. 경계 다형성은 기본적으로 파라 메트릭 다형성과 하위 유형 다형성이 만나는 위치입니다. 형식 생성자가 형식 매개 변수에 대해 완전히 알지 못하는 대신 형식을 특정 형식의 하위 형식으로 바인딩 (또는 제한) 할 수 있습니다 .

컬렉션으로 돌아 갑시다. 해시 테이블을 가져 가십시오. 우리는리스트가 그 요소에 대해 아무것도 알 필요가 없다고 위에서 말했다. 해시 테이블은 해시 할 수 있음을 알아야합니다. (참고 : C #에서는 모든 개체가 동일한 지 비교할 수있는 것처럼 모든 개체를 해시 할 수 있습니다.하지만 모든 언어에 해당되는 것은 아니며 C #에서도 디자인 실수로 간주되는 경우가 있습니다.)

따라서 해시 테이블의 키 유형에 대한 유형 매개 변수를 다음과 같은 인스턴스로 제한하려고합니다 IHashable.

class HashTable<K, V> where K : IHashable
{
  Maybe<V> Get(K key);
  bool Add(K key, V value);
}

대신에 이것을 가지고 있다고 상상해보십시오.

class HashTable
{
    object Get(IHashable key);
    bool Add(IHashable key, object value);
}

당신이 함께 할 것입니다 무엇 value당신은 거기에서 얻을? 당신은 그것으로 아무것도 할 수 없습니다, 당신은 단지 객체라는 것을 알고 있습니다. 그리고 당신이 그것을 반복한다면, 당신이 얻은 것은 당신이 알고있는 한 쌍 IHashable(하나의 속성 만 가지고 있기 때문에 당신에게별로 도움이되지 않습니다 Hash)이고 당신이 알고있는 것이 object(더 적은 것을 도와줍니다)입니다.

또는 귀하의 예를 기반으로 한 것 :

class Repository<T> where T : ISerializable
{
    T Get(int id);
    void Save(T obj);
    void Delete(T obj);
}

디스크에 저장되므로 항목을 직렬화 할 수 있어야합니다. 그러나 이것을 대신하면 어떻게됩니까?

class Repository
{
    ISerializable Get(int id);
    void Save(ISerializable obj);
    void Delete(ISerializable obj);
}

당신이를 넣어 경우 일반적인 경우에, BankAccount에, 당신은 얻을 BankAccount방법 및 특성이 좋아 함께, 다시 Owner, AccountNumber, Balance, Deposit, Withdraw, 등 뭔가 당신이 작업 할 수 있습니다. 다른 경우는? 에 넣었 BankAccount지만 Serializable속성이 하나만있는를 다시 얻습니다 AsString. 그걸로 무엇을 하시겠습니까?

경계 다형성으로 할 수있는 몇 가지 깔끔한 트릭도 있습니다.

F- 바운드 다형성

F- 바운드 정량화는 기본적으로 제약 조건에서 유형 변수가 다시 나타나는 위치입니다. 일부 상황에서는 유용 할 수 있습니다. 예를 들어 어떻게 ICloneable인터페이스 를 작성 합니까? 반환 유형이 구현 클래스의 유형 인 메소드를 작성하는 방법 MyType 기능 이있는 언어에서는 쉽습니다.

interface ICloneable
{
    public this Clone(); // syntax I invented for a MyType feature
}

경계 다형성이있는 언어에서는 다음과 같이 할 수 있습니다.

interface ICloneable<T> where T : ICloneable<T>
{
    public T Clone();
}

class Foo : ICloneable<Foo>
{
    public Foo Clone()
    {
        // …
    }
}

누군가가 "잘못된"클래스를 형식 생성자로 전달하는 것을 막을 수있는 방법이 없기 때문에 MyType 버전만큼 안전하지는 않습니다.

class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
    public SomethingTotallyUnrelatedToBar Clone()
    {
        // …
    }
}

추상 유형 멤버

결과적으로 추상 타입 멤버와 서브 타이핑이 있다면 실제로 파라 메트릭 다형성 없이도 완전히 동일한 작업을 수행 할 수 있습니다. 스칼라는 시작하는 첫 번째 주요 언어되고,이 방향으로 향하고 정확히 예를 들어, 자바와 C #에서 그 반대 인을 제거하는 다음 제네릭합니다.

기본적으로 스칼라에서는 필드와 속성 및 메서드를 멤버로 가질 수있는 것처럼 유형도 가질 수 있습니다. 필드와 속성, 메소드를 추상화하여 나중에 서브 클래스에서 구현할 수있는 것처럼 타입 멤버도 추상화 할 수 있습니다. ListC #에서 지원된다면 다음과 같은 컬렉션으로 돌아가 보겠습니다 .

class List
{
    T; // syntax I invented for an abstract type member
    T Get(int index) { /* … */ }
    void Add(T obj) { /* … */ }
}

class IntList : List
{
    T = int;
}
// this is equivalent to saying `List<int>` with generics

유형에 대한 추상화가 유용하다는 것을 이해합니다. 나는 단지 "실제 생활"에서의 사용을 보지 못합니다. Func <> 및 Task <> 및 Action <>은 라이브러리 유형입니다. 그리고 나는 interface IFoo<T> where T : IFoo<T>또한 기억했다 . 그것은 분명히 실제 응용 프로그램입니다. 좋은 예입니다. 그러나 어떤 이유로 나는 만족스럽지 않다. 차라리 그것이 적절할 때와 그렇지 않을 때 내 마음을 사귀고 싶습니다. 여기에 대한 답변은이 과정에 약간의 기여를하지만, 나는 여전히이 모든 것에 대해 불편 함을 느낍니다. 언어 수준의 문제가 이미 오랫동안 귀찮게하지 않기 때문에 이상합니다.
jungle_mole

좋은 예입니다. 나는 교실에 돌아온 것 같은 느낌이 들었다. +1
Chef_Code

1
@Chef_Code : I 희망 칭찬의 - P이다
요 르그 W MITTAG

그렇습니다. 나는 나중에 이미 언급 한 후에 어떻게 인식 될 수 있을지에 대해 생각했다. 성실을 확인하기 위해 ... 그렇습니다.
Chef_Code

14

요점은 다음과 같습니다. 제네릭은 컬렉션을 제외한 모든 것에 유용하며 클래스 내에서 제네릭 형식 매개 변수 대신이 유형 제약 조건을 사용하면 형식 제약 조건이 특수화 된 것으로 일반화하지 않습니까?

아니 당신은 너무 많은에 대한 생각 Repository, 그것은 곳 입니다 거의 동일. 그러나 그것이 제네릭이 아닙니다. 그들은 사용자를 위해 있습니다.

여기서 중요한 점은 리포지토리 자체가 더 일반적이라는 것이 아닙니다. 그것은 사용자가 있음을의 더 specialized- 즉, 있음 Repository<BusinessObject1>Repository<BusinessObject2>다른 유형, 또한, 내가 걸릴 경우 점이다 Repository<BusinessObject1>, 내가 알고 내가 얻을 것이다 BusinessObject1으로부터 다시는 Get.

간단한 상속에서이 강력한 타이핑을 제공 할 수 없습니다. 제안 된 리포지토리 클래스는 사람들이 다른 종류의 비즈니스 개체에 대한 리포지토리를 혼동하거나 올바른 종류의 비즈니스 개체가 다시 나오는 것을 막기 위해 아무 것도하지 않습니다.


고마워요. 그러나 IntelliSense를 사용하지 않는 사용자를 돕는 것처럼 간단하게이 고급 언어 기능을 사용하는 것이 간단합니까? (나는 조금 과장하지만, 당신이 요점을 확신합니다)
jungle_mole

@zloidooraque : 또한 IntelliSense는 어떤 종류의 객체가 저장소에 저장되어 있는지 알지 못합니다. 그러나 네, 대신 캐스트를 사용하려는 경우 제네릭없이 아무것도 할 수 있습니다.
gexicide

@gexicide 요점 : 일반적인 인터페이스를 사용하면 캐스트를 어디에서 사용해야하는지 알 수 없습니다. 나는 "사용 Object" 이라고 말한 적이 없다 . 또한 컬렉션을 작성할 때 제네릭을 사용하는 이유를 이해합니다 (건조 원칙). 아마도 내 초기 질문은 컬렉션 컨텍스트 외부에서 제네릭을 사용하는 것과 관련이 있었을 것입니다.
jungle_mole

@zloidooraque : 환경과 관련이 없습니다. 이 경우 인텔리 당신을 말할 수 없다 IBusinessObjectA는 BusinessObject1또는를 BusinessObject2. 알 수없는 파생 형식을 기반으로 오버로드를 해결할 수 없습니다. 잘못된 유형으로 전달되는 코드는 거부 할 수 없습니다. Intellisense가 전혀 할 수없는 강력한 타이핑의 백만 비트가 있습니다. 더 나은 툴링 지원은 큰 이점이지만 실제로는 핵심 이유와 관련이 없습니다.
DeadMG

@DeadMG 그리고 그것은 내 요점입니다 : intellisense는 그것을 할 수 없습니다 : 제네릭을 사용하여 그렇게 할 수 있습니까? 상관이 있나? 인터페이스로 객체를 가져올 때 왜 다운 캐스트합니까? 그렇게하면 나쁜 디자인입니까? 그리고 왜 "오버로드 해결"입니까? 사용자는 올바른 방법의 호출을 시스템에 다형성 (polymorphism) 인 경우 파생 된 유형에 따라 호출 방법을 결정하지 않아야합니다. 그리고 이것은 다시 질문으로 연결됩니다 : 제네릭은 컨테이너 외부에서 유용합니까? 나는 당신과 말다툼하고 있지 않다, 나는 그것을 정말로 이해해야한다.
jungle_mole

12

"이 리포지토리의 클라이언트는 IBusinessObject 인터페이스를 통해 개체를 가져오고 사용하려고합니다."

아니요, 그렇지 않습니다.

IBusinessObject에 다음과 같은 정의가 있다고 가정하십시오.

public interface IBusinessObject
{
  public int Id { get; }
}

모든 비즈니스 개체간에 유일한 공유 기능이기 때문에 ID 만 정의합니다. 그리고 당신은 두 개의 실제 비즈니스 객체를 가지고 있습니다 : Person과 Address (사람들은 거리가없고 주소는 이름이 없기 때문에 둘 다의 기능을 가진 commom 인터페이스로 제한 할 수 없습니다. 인터페이스 Segragation 원리 에있는 "I" SOLID )

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

이제 일반 버전의 저장소를 사용하면 어떻게됩니까?

public class Repository<T> where T : class, IBusinessObject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

일반 리포지토리에서 Get 메서드를 호출하면 반환 된 개체가 강력하게 형식화되어 모든 클래스 멤버에 액세스 할 수 있습니다.

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

반면, 일반이 아닌 리포지토리를 사용하는 경우 :

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

IBusinessObject 인터페이스에서만 멤버에 액세스 할 수 있습니다.

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

따라서 다음 코드로 인해 이전 코드가 컴파일되지 않습니다.

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

물론, IBussinesObject를 실제 클래스로 캐스트 할 수는 있지만 제네릭이 허용하는 모든 컴파일 타임 마법을 잃어 버릴 수 있습니다 (불량한 길에서 InvalidCastExceptions로 이어짐). 불필요하게 캐스트 오버 헤드로 고통받을 것입니다 ... 컴파일 타임은 성능을 검사하지 않아야합니다 (필요하지 않음).

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.