제네릭 메서드 다중 (OR) 형식 제약 조건


135

이것을 읽으 면서 메소드가 일반적인 메소드로 만들어 여러 유형의 매개 변수를 허용 할 수 있다는 것을 알게되었습니다. 이 예에서 다음 코드는 유형 제약 조건과 함께 사용되어 "U"가입니다 IEnumerable<T>.

public T DoSomething<U, T>(U arg) where U : IEnumerable<T>
{
    return arg.First();
}

다음과 같은 여러 유형 제약 조건을 추가 할 수있는 코드가 더 있습니다.

public void test<T>(string a, T arg) where T: ParentClass, ChildClass 
{
    //do something
}

그러나이 코드 argParentClass 유형이어야합니다 ChildClass. 내가하고 싶은 것은 arg가 유형 ParentClass 이거나 ChildClass 다음과 같은 방식 일 수 있다는 것입니다.

public void test<T>(string a, T arg) where T: string OR Exception
{
//do something
}

당신의 도움은 항상 감사합니다!


4
이러한 메소드의 본문 내에서 일반적인 방식으로 여러 가지 유형이 모두 특정 기본 클래스에서 파생되지 않는 한 유용하게 수행 할 수 있는 경우는 무엇입니까?
Damien_The_Unbeliever

@Damien_The_Unbeliever 몸이 무슨 뜻인지 잘 모르시겠습니까? 어떤 유형을 허용하고 수동으로 본문을 검사한다는 의미가 아니라면 실제 코드에서 작성하고 싶습니다 (마지막 코드 스 니펫). 문자열 OR 예외를 전달할 수 있기를 원하므로 클래스가 없습니다. 관계 (내가 상상 한 system.object 제외).
맨스필드

1
또한 서면으로 아무 소용이 없다 참고 where T : string로, stringA는 봉인 클래스. 당신이 할 수있는 유일한 유용한 것은 아래의 대답에서 @ Botz3000에 설명 된 것처럼 stringand T : Exception에 대한 과부하를 정의하는 것입니다 .
Mattias Buelens

그러나 관계가 없으면 호출 할 수있는 유일한 방법 arg은 다음과 같이 정의 된 것입니다 object. 따라서 그림에서 제네릭을 제거하고 유형을 만드십시오 arg object. 무엇을 얻고 있습니까?
Damien_The_Unbeliever

1
@Mansfield 객체 매개 변수를 허용하는 개인 메서드를 만들 수 있습니다. 두 과부하 모두 그 중 하나를 호출합니다. 여기에는 제네릭이 필요하지 않습니다.
Botz3000

답변:


68

불가능합니다. 그러나 특정 유형에 대한 과부하를 정의 할 수 있습니다.

public void test(string a, string arg);
public void test(string a, Exception arg);

그것들이 제네릭 클래스의 일부라면, 제네릭 버전의 메소드보다 선호됩니다.


1
흥미 롭군 이것은 나에게 일어 났지만 코드를 하나의 함수를 사용하여 더 깨끗하게 만들 수 있다고 생각했습니다. 아, 고마워요! 호기심 때문에, 이것이 불가능한 특별한 이유가 있는지 아십니까? (고의로 언어에서 제외 되었습니까?)
Mansfield

3
@Mansfield 정확한 이유를 모르겠지만 더 이상 의미있는 방식으로 일반 매개 변수를 사용할 수 없다고 생각합니다. 수업 내에서 완전히 다른 유형이 허용되면 객체처럼 취급해야합니다. 즉, 일반 매개 변수를 제외하고 과부하를 제공 할 수 있습니다.
Botz3000

3
@Mansfield, 그것은 or관계가 일반적인 것들을 유용하게 만들기 때문입니다. 당신은 줄 할 모든 것을하는 알아낼 반사를 할 수 있습니다. (왝!).
Chris Pfohl

29

Botz의 대답은 100 % 정확합니다. 간단한 설명은 다음과 같습니다.

메소드를 작성하거나 (일반적이든 아니든) 메소드가 취하는 매개 변수 유형을 선언하면 계약을 정의하는 것입니다.

Type T가 수행하는 방법을 알고있는 일련의 작업을 수행하는 방법을 알고있는 객체를 제공하면 'a': 선언 한 유형의 반환 값 또는 'b': 사용하는 일종의 동작을 전달할 수 있습니다 그 유형.

한 번에 두 개 이상의 유형을 제공하려고하거나 (또는 ​​or를 가짐) 계약이 퍼지되는 둘 이상의 유형이 될 수있는 값을 리턴하려고 시도하는 경우 :

줄넘기를하는 방법을 알고 있거나 pi를 15 자리로 계산하는 방법을 알고있는 물체를 주면 낚시를하거나 콘크리트를 섞을 수있는 물체를 돌려줍니다.

문제는 당신이 방법에 들어갈 때 그들이 당신에게 또는를 주 었는지 전혀 모른다 IJumpRope는 것 PiFactory입니다. 또한, 계속 진행하고 (매직 컴파일하기 위해 가정했다고 가정 할 때) 메소드를 사용할 때 실제로 Fisher또는AbstractConcreteMixer . 기본적으로 그것은 모든 것을 더 혼란스럽게 만듭니다.

문제에 대한 해결책은 다음 두 가지 가능성 중 하나입니다.

  1. 각각의 가능한 변환, 동작 등을 정의하는 둘 이상의 방법을 정의하십시오. 보츠의 대답입니다. 프로그래밍 세계에서는이를 메서드 오버로딩이라고합니다.

  2. 메소드에 필요한 모든 작업을 수행하는 방법을 알고 기본 클래스 또는 인터페이스를 정의하고 하나의 메소드 해당 유형을 갖도록하십시오 . 구현에 매핑하는 방법을 정의 하기 위해 a stringException작은 클래스를 래핑하는 것이 포함될 수 있지만 모든 것이 매우 명확하고 읽기 쉽습니다. 나는 4 년 후에 와서 코드를 읽고 무슨 일이 일어나고 있는지 쉽게 이해할 수있었습니다.

선택하는 것은 선택 1과 2가 얼마나 복잡한 지와 얼마나 확장 가능한지에 달려 있습니다.

따라서 특정 상황에 대해 예외에서 메시지 또는 무언가를 꺼내고 있다고 상상할 것입니다.

public interface IHasMessage
{
    string GetMessage();
}

public void test(string a, IHasMessage arg)
{
    //Use message
}

이제 a string와 a Exception를 IHasMessage로 변환하는 메소드 만 있으면됩니다. 아주 쉽게.


죄송합니다 @ Botz3000, 방금 이름이 틀 렸습니다.
Chris Pfohl

또는 컴파일러가 함수 내에서 형식을 공용체 형식으로 위협하고 반환 값의 형식이 같은 형식 일 수 있습니다. TypeScript가이를 수행합니다.
Alex

@Alex 그러나 그것은 C # 이하는 일이 아닙니다.
Chris Pfohl

사실이지만 가능합니다. 나는이 대답을 읽을 수 없다는 것을 읽었습니다. 잘못 해석 했습니까?
Alex

1
문제는 C # 제네릭 매개 변수 제약 조건과 제네릭 자체가 C ++ 템플릿에 비해 매우 원시적이라는 것입니다. C #에서는 컴파일러에 제네릭 형식에 어떤 작업이 허용되는지 미리 알려 주어야합니다. 이 정보를 제공하는 방법은 구현 인터페이스 제약 조건 (여기서 T : IDisposable)을 추가하는 것입니다. 그러나 타입이 일반적인 메소드를 사용하기 위해 일부 인터페이스를 구현하기를 원하지 않거나 일반적인 인터페이스가없는 일반적인 코드에서 일부 유형을 허용하고 싶을 수도 있습니다. 전의. 값 기반 비교를 위해 단순히 Equals (v1, v2)를 호출 할 수 있도록 구조체 나 문자열을 허용하십시오.
Vakhtang

8

ChildClass가 ParentClass에서 파생 된 것을 의미하는 경우 ParentClass와 ChildClass를 모두 허용하도록 다음을 작성할 수 있습니다.

public void test<T>(string a, T arg) where T: ParentClass 
{
    //do something
}

반면에 상속 관계가없는 두 가지 유형을 사용하려면 동일한 인터페이스를 구현하는 유형을 고려해야합니다.

public interface ICommonInterface
{
    string SomeCommonProperty { get; set; }
}

public class AA : ICommonInterface
{
    public string SomeCommonProperty
    {
        get;set;
    }
}

public class BB : ICommonInterface
{
    public string SomeCommonProperty
    {
        get;
        set;
    }
}

그런 다음 일반 함수를 다음과 같이 작성할 수 있습니다.

public void Test<T>(string a, T arg) where T : ICommonInterface
{
    //do something
}

위의 주석에 따라 봉인 된 클래스 인 문자열을 사용하고 싶기 때문에이 작업을 수행 할 수 없을 것입니다.
Mansfield

이것은 실제로 디자인 문제입니다. 일반 함수는 코드 재사용으로 유사한 작업을 수행하는 데 사용됩니다. 메소드 본문에서 다른 조작을 수행하려는 경우 메소드를 분리하는 것이 더 좋은 방법입니다 (IMHO).
daryal

실제로 내가하고있는 일은 간단한 오류 로깅 기능을 작성하는 것입니다. 마지막 매개 변수는 오류에 대한 정보 문자열이거나 예외입니다.이 경우 e.message + e.stacktrace를 문자열로 저장합니다.
맨스필드

isSuccesful, 메시지 및 예외를 속성으로 저장하여 새 클래스를 작성할 수 있습니다. 그런 다음 성공 여부를 확인하고 나머지를 수행 할 수 있습니다.
daryal

1

이 질문만큼 오래된 것은 위의 설명에서 여전히 임의의 공감대를 얻습니다. 설명은 여전히 ​​완벽하게 잘 유지되지만, 나는 유니온 타입 (C #에서 직접 지원하지 않는 질문에 대한 강력한 형식의 답변) 대신 나에게 도움이되는 유형으로 두 번째로 대답 할 것입니다. ).

using System;
using System.Diagnostics;

namespace Union {
    [DebuggerDisplay("{currType}: {ToString()}")]
    public struct Either<TP, TA> {
        enum CurrType {
            Neither = 0,
            Primary,
            Alternate,
        }
        private readonly CurrType currType;
        private readonly TP primary;
        private readonly TA alternate;

        public bool IsNeither => currType == CurrType.Primary;
        public bool IsPrimary => currType == CurrType.Primary;
        public bool IsAlternate => currType == CurrType.Alternate;

        public static implicit operator Either<TP, TA>(TP val) => new Either<TP, TA>(val);

        public static implicit operator Either<TP, TA>(TA val) => new Either<TP, TA>(val);

        public static implicit operator TP(Either<TP, TA> @this) => @this.Primary;

        public static implicit operator TA(Either<TP, TA> @this) => @this.Alternate;

        public override string ToString() {
            string description = IsNeither ? "" :
                $": {(IsPrimary ? typeof(TP).Name : typeof(TA).Name)}";
            return $"{currType.ToString("")}{description}";
        }

        public Either(TP val) {
            currType = CurrType.Primary;
            primary = val;
            alternate = default(TA);
        }

        public Either(TA val) {
            currType = CurrType.Alternate;
            alternate = val;
            primary = default(TP);
        }

        public TP Primary {
            get {
                Validate(CurrType.Primary);
                return primary;
            }
        }

        public TA Alternate {
            get {
                Validate(CurrType.Alternate);
                return alternate;
            }
        }

        private void Validate(CurrType desiredType) {
            if (desiredType != currType) {
                throw new InvalidOperationException($"Attempting to get {desiredType} when {currType} is set");
            }
        }
    }
}

상기 클래스가 될 수있는 형식 나타내는 하나 TP 또는 TA를. 당신은 그것을 그대로 사용할 수 있습니다 (유형은 원래의 대답으로 되돌아갑니다) :

// ...
public static Either<FishingBot, ConcreteMixer> DemoFunc(Either<JumpRope, PiCalculator> arg) {
  if (arg.IsPrimary) {
    return new FishingBot(arg.Primary);
  }
  return new ConcreteMixer(arg.Secondary);
}

// elsewhere:

var fishBotOrConcreteMixer = DemoFunc(new JumpRope());
var fishBotOrConcreteMixer = DemoFunc(new PiCalculator());

중요 사항 :

  • 확인하지 않으면 런타임 오류가 발생합니다 IsPrimary먼저 .
  • IsNeither IsPrimary또는 중 하나를 확인할 수 있습니다 IsAlternate.
  • 당신은 통해 값에 액세스 할 수 있습니다 PrimaryAlternate
  • TP / TA와 둘 사이에 암시 적 변환기가있어 값이나 Either예상되는 위치 를 전달할 수 있습니다 . 당신 통과하면EitherTA이상이 TP예상된다하지만,이 Either값의 잘못된 유형을 포함하면 런타임 오류가 발생합니다.

나는 일반적으로 메소드가 결과 또는 오류를 반환하기를 원할 때 이것을 사용합니다. 실제로 해당 스타일 코드를 정리합니다. 나는 또한 메소드 오버로드를 대신하여 이것을 거의 사용 하지 않는 경우도있다 . 실제로 이것은 과부하를 대체하기에 매우 열악합니다.

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