연산자 - c# 제네릭 인자



특정 생성자를 갖도록 제네릭 형식 매개 변수 제한 (2)

단일의 int 파라미터를 사용해 인스턴스를 생성 할 수있는 제네릭 타입 T 를 가지는 메소드를 원한다면, T 형 이외에 Func<int, T> 또는 accept 인터페이스, 아마도 같은 것을 사용 :

static class IFactoryProducing<ResultType>
{
    interface WithParam<PT1>
    {
        ResultType Create(PT1 p1);
    }
    interface WithParam<PT1,PT2>
    {
        ResultType Create(PT1 p1, PT2 p2);
    }
}

(외부 정적 클래스를 인터페이스로 선언 할 수 있으면 코드가 더 좋아 보이지만 IFactoryProducing<T>.WithParam<int>IFactory<int,T> 보다 명확 해 보입니다 (후자는 매개 변수가 모호하기 때문에 그 결과입니다).

어쨌든 하나의 Aroud 유형 T 통과 할 때마다 적합한 공장 위임자 또는 인터페이스를 통과하기 때문에 매개 변수화 된 생성자 제약 조건으로 달성 할 수있는 것의 99 %를 달성 할 수 있습니다. 생성 가능한 각 유형이 팩토리의 정적 인스턴스를 생성하도록함으로써 런타임 비용을 최소화 할 수 있으므로 모든 종류의 루프 컨텍스트에서 팩토리 인스턴스를 작성할 필요가 없습니다.

BTW는 기능의 비용을 넘어서는 거의 확실한 해결 방법보다 다재다능한 실질적인 제한이 있습니다. 생성자 제약 조건이 매개 변수 유형에 반비례하지 않는 경우, 사용되는 매개 변수의 실제 유형 외에 생성자 제약 조건에 필요한 정확한 유형에 대한 유형 매개 변수를 전달해야 할 수도 있습니다. 그 때까지는 공장을 지나칠 수도 있습니다. 만약 contravariant라면 generic 타입이 new(Cat, ToyotaTercel) 제약을 가지고 있고 실제 타입이 new(Animal, ToyotaTercel)new(Cat, Automobile) 가진다면 생성자를 호출해야하는 문제를 해결해야한다.

추 신 : 문제를 명확히하기 위해 반 변형 된 생성자 제약이 "이중 다이아몬드"문제의 변형을 유도합니다. 중히 여기다:

T CreateUsingAnimalAutomobile<T>() where T:IThing,new(Animal,Automobile)
{ ... }

T CreateUsingAnimalToyotaTercel<T>() where T:IThing,new(Animal,ToyotaTercel)
{ return CreateUsingAnimalAutomobile<T>(); }

T CreateUsingCatAutomobile<T>() where T:IThing,new(Cat,Automobile)
{ return CreateUsingAnimalAutomobile<T>(); }

IThing thing1=CreateUsingAnimalToyotaTercel<FunnyClass>(); // FunnyClass defined in question
IThing thing2=CreateUsingCatAutomobile<FunnyClass>(); // FunnyClass defined in question

CreateUsingAnimalToyotaTercel<FunnyClass>() 대한 호출을 처리 할 때 "Animal, ToyotaTercel"생성자는 해당 메서드에 대한 제약 조건을 충족해야하며 해당 메서드의 제네릭 형식은 CreateUsingAnimalAutomobile<T>() 대한 제약 조건을 충족해야합니다. CreateUsingCatAutomobile<FunnyClass>() 대한 호출을 처리 할 때 "Cat, Automobile"생성자는 해당 메서드에 대한 제약 조건을 충족해야하며 해당 메서드의 제네릭 형식은 CreateUsingAnimalAutomobile<T>() 대한 제약 조건을 충족해야합니다.

문제는 두 호출 모두 동일한 CreateUsingAnimalAutomobile<SillyClass>() 메서드에 대한 호출을 호출하고 해당 메서드는 호출해야하는 생성자를 알 수있는 방법이 없다는 것입니다. Contravariance와 관련된 모호성은 생성자에게 고유하지 않지만 대부분의 경우 컴파일 타임 바인딩을 통해 해결됩니다.

https://src-bin.com

왜 제네릭 형식 매개 변수에 대한 새로운 제약 조건이 매개 변수없이 적용될 수 있는지 알고 싶습니다. 즉, 매개 변수없는 생성자를 갖기 위해 형식을 제한 할 수 있습니다. 그러나 클래스에 제약 조건이없는 생성자를 제약 할 수는 없습니다. int를 매개 변수로받습니다. 리플렉션이나 팩토리 패턴을 사용하여이 문제를 해결할 수있는 방법을 알고 있습니다. 그러나 나는 그것에 대해 생각 해왔고, 매개 변수가없는 생성자와 새로운 제약 조건에 대한이 제한을 정당화 할 수있는 매개 변수가있는 생성자 사이의 차이를 실제로 생각할 수 없기 때문에 이유를 알고 싶습니다. 내가 뭘 놓치고 있니? 고마워.

인수 1 : 생성자가 메서드입니다.

@ 에릭 : 잠깐 너와 함께 나 여기에 가자.

생성자는 메소드입니다.

그럼 내가 이렇게 갈 수 없다면 아무도 반대하지 않을 거라고 생각해.

public interface IReallyWonderful
{
    new(int a);

    string WonderMethod(int a);
}

하지만 일단 그렇게되면, 나는 갈 것입니다.

public class MyClass<T>
        where T : IReallyWonderful
{
    public string MyMethod(int a, int b)
    {
        T myT = new T(a);
        return myT.WonderMethod(b);
    }
}

내가 처음에하고 싶었던 것입니다. 죄송 합니다만, 아니요, 생성자는 메서드가 아니 거나 최소한 정확하지는 않습니다.

이 기능을 구현하는 어려움에 대해 잘 알지는 못했을 것입니다. 그렇더라도 주주 돈의 현명한 지출에 관한 결정에 대해서는 말할 것도 없습니다. 그런 식으로, 나는 곧 대답으로 표시했을 것이다.

학업 (내) 관점에서, 그리고 구현 비용에 대해 아무런 고려도없이 문제는 정말로 (지난 몇 시간 동안 이것을 반올림했습니다) :

생성자를 클래스 구현의 일부로 간주하거나 의미 계약의 일부로 간주해야합니다 (인터페이스가 의미 계약으로 간주되는 것과 같은 방식으로).

구현의 일부로 생성자를 고려한 경우 제네릭 형식 매개 변수의 생성자를 제한하는 것은 매우 일반적인 일이 아닙니다. 제네릭 형식을 구체적인 구현에 묶는 것이므로 거의 이유를 말할 수 있습니다. 제네릭을 전혀 사용하지 않습니까?

구현의 일부인 생성자의 예 ( ITransformer 정의한 의미 적 계약의 일부로 다음 생성자 중 하나를 지정하는 것은 의미가 ITransformer ) :

public interface ITransformer
{
    //Operates with a and returns the result;
    int Transform(int a);
}

public class PlusOneTransformer : ITransformer
{
    public int Transform(int a)
    {
        return a + 1;
    }
}

public class MultiplyTransformer : ITransformer
{
    private int multiplier;

    public MultiplyTransformer(int multiplier)
    {
        this.multiplier = multiplier;
    }

    public int Transform(int a)
    {
        return a * multiplier;
    }
}

public class CompoundTransformer : ITransformer
{
    private ITransformer firstTransformer;
    private ITransformer secondTransformer;

    public CompoundTransformer(ITransformer first, ITransformer second)
    {
        this.firstTransformer = first;
        this.secondTransformer = second;
    }

    public int Transform(int a)
    {
        return secondTransformer.Transform(firstTransformer.Transform(a));
    }
}

문제는 생성자가 다음과 같이 의미 론적 계약의 일부로 간주 될 수 있다는 것입니다.

public interface ICollection<T> : IEnumerable<T>
{
    new(IEnumerable<T> tees);

    void Add(T tee);

    ...
}

즉, 요소 ​​시퀀스에서 컬렉션을 작성하는 것이 항상 가능합니다. 맞습니까? 그리고 그것은 의미 론적 계약의 매우 유효한 부분을 만들 것입니다, 그렇죠?

나, 주주 돈의 현명한 지출에 관한 측면을 고려하지 않고, 의미 계약의 일부로 생성자를 허용하는 편이 낫다. 어떤 개발자는 그것을 엉망으로 만들고 어떤 타입을 의미 론적으로 잘못된 생성자를 갖도록 제한합니다. 음, 같은 개발자가 의미 론적으로 잘못된 연산을 추가하면 어떤 차이가 있습니까? 어쨌든 의미 론적 계약은 우리 모두가 동의했기 때문에, 그리고 우리 모두가 우리 도서관을 정말로 잘 문서화했기 때문에 그것입니다.)

인수 2 : 생성자를 해석 할 때 가정 된 문제

@supercat은 몇 가지 예를 어떻게 설정하는지 (주석에서 인용)

또한 생성자 제약 조건이 어떻게 작동해야 하는지를 정확하게 정의하는 것은 놀라운 일이 아닙니다.

그러나 나는 정말로 반대해야한다. C # (글쎄, .NET에서)과 같은 "펭귄을 만드는 법"과 같은 놀라움이 있습니다. 단순히 일어나지 않습니다. 컴파일러가 메서드 호출을 해결하는 방법에 대한 꽤 간단한 규칙이 있습니다. 컴파일러가이를 해결할 수 없다면, 잘 통과하지 못하고 컴파일되지 않습니다.

그의 마지막 예는 :

만약 contravariant라면 generic 타입이 new (Cat, ToyotaTercel) 제약을 가지고 있고 실제 타입이 new (Animal, ToyotaTercel)와 new (Cat, Automobile)를 가진다면 생성자를 호출해야하는 문제를 해결해야한다.

글쎄, 이것을 시도해 볼 수있다. (내 견해로는 @supercat에 의해 제안 된 것과 비슷한 상황이다)

class Program
{
    static void Main(string[] args)
    {
        Cat cat = new Cat();
        ToyotaTercel toyota = new ToyotaTercel();

        FunnyMethod(cat, toyota);
    }

    public static void FunnyMethod(Animal animal, ToyotaTercel toyota)
    {
        Console.WriteLine("Takes an Animal and a ToyotaTercel");
    }

    public static void FunnyMethod(Cat cat, Automobile car)
    {
        Console.WriteLine("Takes a Cat and an Automobile");
    }
}

public class Automobile
{ }

public class ToyotaTercel : Automobile
{ }

public class Animal
{ }

public class Cat : Animal
{ }

그리고, 와우, 그것은 오류와 함께 컴파일되지 않습니다

'TestApp.Program.FunnyMethod (TestApp.Animal, TestApp.ToyotaTercel)'및 'TestApp.Program.FunnyMethod (TestApp.Cat, TestApp.Automobile)'메서드 또는 속성 사이의 호출이 모호합니다.

동일한 문제가 매개 변수화 된 생성자 제약 조건이있는 솔루션에서 발생하는 경우 결과가 달라져야하는 이유는 다음과 같습니다.

class Program
{
    static void Main(string[] args)
    {
        GenericClass<FunnyClass> gc = new GenericClass<FunnyClass>();
    }
}

public class Automobile
{ }

public class ToyotaTercel : Automobile
{ }

public class Animal
{ }

public class Cat : Animal
{ }

public class FunnyClass
{
    public FunnyClass(Animal animal, ToyotaTercel toyota)
    {            
    }

    public FunnyClass(Cat cat, Automobile car)
    {            
    }
}

public class GenericClass<T>
   where T: new(Cat, ToyotaTercel)
{ }

이제 컴파일러는 생성자에 대한 제약 조건을 처리 할 수 ​​없지만 가능한 경우 GenericClass<FunnyClass> gc = new GenericClass<FunnyClass>(); 줄에서 오류가 발생하지 않는 이유는 무엇입니까 GenericClass<FunnyClass> gc = new GenericClass<FunnyClass>(); FunnyMethod 의 첫 번째 예제를 컴파일 할 때 얻은 것과 비슷합니다.

어쨌든 한 걸음 더 나아가겠습니다. 추상 메소드를 오버라이드하거나 인터페이스에 정의 된 메소드를 구현할 때 동일한 매개 변수 유형을 사용해야하며 상속자 또는 조상은 허용되지 않습니다. 따라서 매개 변수화 된 생성자가 필요한 경우 요구 사항은 다른 정의가 아닌 정확한 정의를 충족해야합니다. 이 경우 GenericClass 클래스의 generic 매개 변수 유형에 대해 FunnyClass 클래스를 형식으로 지정할 수 없습니다.


Answer #1

커크 월 (Kirk Woll)의 말은 물론 필요합니다. 우리는 존재하지 않는 기능에 대한 정당성을 제공 할 필요가 없습니다. 기능에는 엄청난 비용이 듭니다.

그러나이 특별한 경우에 필자는 미래의 언어 버전을위한 가능한 기능으로 디자인 회의에서 등장한 경우 필자가이 기능을 다시 푸시하는 몇 가지 이유를 분명히 제공 할 수 있습니다.

시작하기 : 더 일반적인 기능을 고려하십시오. 생성자는 메서드 입니다. "형식 인수가 int를받는 생성자를 가져야합니다"라고 말할 수있는 방법이 있기를 기대한다면 "형식 인수에는 Q라는 이름의 공용 메서드가 있어야하며 두 개의 정수를 사용하여 끈?"

string M<T>(T t) where T has string Q(int, int)
{
    return t.Q(123, 456);
}

그것은 당신을 아주 일반적인 일로 생각합니까? 이런 종류의 제약이있는 것은 제네릭에 대한 생각과 반대되는 것처럼 보입니다.

이 기능이 메서드에 나쁜 생각 인 경우 생성자가 되는 메서드는 왜 좋은 아이디어입니까?

반대로, 메소드와 생성자에 대한 좋은 아이디어라면, 그곳에서 멈추어야 할 이유가 무엇입니까?

string M<T>(T t) where T has a field named x of type string
{
    return t.x;
}

나는 우리가 전체 기능을 수행 하거나 전혀하지 말아야한다고 말한다. 유형이 특정 생성자를 갖도록 제한하는 것이 중요하다면 생성자 만이 아닌 일반 구성원을 기준으로 전체 기능을 수행하고 유형을 제한합시다.

물론이 기능은 설계, 구현, 테스트, 문서화 및 유지 보수에 훨씬 많은 비용이 듭니다.

두 번째 요점 : "just constructors"버전 또는 "any member"버전 중 하나를 구현하기로 결정했다고 가정 해보십시오. 우리는 어떤 코드를 생성합니까? generic codegen은 정적 분석을 한 번 수행하고 완료 할 수 있도록 신중하게 설계되었습니다. 그러나 IL에서 int를 취하는 생성자를 호출하는 표준 방법은 없습니다. IL에 새로운 개념을 추가하거나 일반 생성자 호출이 Reflection을 사용하도록 코드를 생성해야합니다 .

전자는 비싸다. IL의 기본 개념을 변경하는 것은 매우 많은 비용이 듭니다. 후자는 (1) 느리고, (2) 매개 변수를 상자에 넣고, (3) 자신이 작성할 수있는 코드입니다. 리플렉션을 사용하여 생성자를 찾고 호출하려면 리플렉션을 사용 하는 코드작성하여 생성자를 찾고 호출하십시오. 이것이 코드 gen 전략이라면 제약 조건이 부여하는 유일한 이점은 int를 취하는 public ctor가없는 형식 인수를 전달하는 버그가 런타임 대신 컴파일 타임에 잡히는 것입니다 . 반사 및 복싱 벌칙을 피하는 것과 같은 제네릭의 다른 이점을 얻지 못합니다.





new-operator