C # Generics-중복 방법을 피하는 방법?


28

다음과 같은 두 개의 클래스가 있다고 가정합니다 (첫 번째 코드 블록과 일반적인 문제는 C #과 관련이 있음).

class A 
{
    public int IntProperty { get; set; }
}

class B 
{
    public int IntProperty { get; set; }
}

이러한 클래스는 어떤 식 으로든 변경할 수 없습니다 (타사 어셈블리의 일부 임). 따라서 동일한 인터페이스를 구현하거나 IntProperty를 포함하는 동일한 클래스를 상속 할 수 없습니다.

IntProperty두 클래스 의 속성에 몇 가지 논리를 적용하고 싶습니다 .C ++에서는 템플릿 클래스를 사용하여 쉽게 수행 할 수 있습니다.

template <class T>
class LogicToBeApplied
{
    public:
        void T CreateElement();

};

template <class T>
T LogicToBeApplied<T>::CreateElement()
{
    T retVal;
    retVal.IntProperty = 50;
    return retVal;
}

그리고 다음과 같이 할 수 있습니다.

LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();   

그렇게하면 ClassA와 ClassB 모두에서 작동하는 단일 일반 팩토리 클래스를 만들 수 있습니다.

그러나 C #에서는 where논리 코드가 정확히 동일하더라도 두 개의 다른 절로 두 개의 클래스를 작성해야합니다 .

public class LogicAToBeApplied<T> where T : ClassA, new()
{
    public T CreateElement()
    {
        T retVal = new T();
        retVal.IntProperty = 50;
        return retVal;
    }
}

public class LogicBToBeApplied<T> where T : ClassB, new()
{
    public T CreateElement()
    {
        T retVal = new T();
        retVal.IntProperty = 50;
        return retVal;
    }
}

나는 다른 수업을 원한다면 where절 위에서 설명한 의미에서 동일한 코드를 적용하려면 관련 클래스, 즉 동일한 클래스를 상속해야합니다. 두 가지 완전히 동일한 방법을 사용하는 것은 매우 성가신 일입니다. 또한 성능 문제로 인해 리플렉션을 사용하고 싶지 않습니다.

누군가가 이것을보다 우아한 방식으로 작성할 수있는 접근법을 제안 할 수 있습니까?


3
처음에 왜 제네릭을 사용합니까? 이 두 가지 기능에 대한 일반적인 내용은 없습니다.
Luaan

1
@Luaan 추상 팩토리 패턴의 변형에 대한 간단한 예입니다. ClassA 또는 ClassB를 상속하는 수십 개의 클래스가 있고 ClassA와 ClassB가 추상 클래스라고 가정하십시오. 상속 된 클래스에는 추가 정보가 없으므로 인스턴스화해야합니다. 각각에 대한 팩토리를 작성하는 대신 제네릭을 사용하기로 결정했습니다.
블라디미르 스토 키

6
글쎄, 당신은 그들이 다음 릴리스에서 그것을 깨뜨리지 않을 것이라고 확신한다면 반사 나 역학을 사용할 수 있습니다.
Casey

이것은 실제로 제네릭에 대한 가장 큰 불만은 이것이 불가능하다는 것입니다.
Joshua

1
@Joshua, "duck 타이핑"을 지원하지 않는 인터페이스의 문제라고 생각합니다.
Ian

답변:


49

프록시 인터페이스 (때로는 미묘한 차이가 있는 어댑터 라고도 함 ) LogicToBeApplied를 추가하고 프록시 측면 에서 구현 한 다음 두 개의 람다 (속성 get 및 집합에 대한 하나)에서이 프록시 인스턴스를 구성하는 방법을 추가하십시오.

interface IProxy
{
    int Property { get; set; }
}
class LambdaProxy : IProxy
{
    private Function<int> getFunction;
    private Action<int> setFunction;
    int Property
    {
        get { return getFunction(); }
        set { setFunction(value); }
    }
    public LambdaProxy(Function<int> getter, Action<int> setter)
    {
        getFunction = getter;
        setFunction = setter;
    }
}

이제 IProxy를 통과해야하지만 타사 클래스의 인스턴스가있을 때마다 람다를 전달할 수 있습니다.

A a = new A();
B b = new B();
IProxy proxyA = new LambdaProxy(() => a.Property, (val) => a.Property = val);
IProxy proxyB = new LambdaProxy(() => b.Property, (val) => b.Property = val);
proxyA.Property = 12; // mutates the proxied `a` as well

또한 간단한 도우미를 작성하여 A 또는 B 인스턴스에서 LamdaProxy 인스턴스를 생성 할 수 있습니다. 확장형 메서드 일 수도 있습니다.

public static class ProxyExtension
{
    public static IProxy Proxied(this A a)
    {
      return new LambdaProxy(() => a.Property, (val) => a.Property = val);
    }

    public static IProxy Proxied(this B b)
    {
      return new LambdaProxy(() => b.Property, (val) => b.Property = val);
    }
}

이제 프록시 구성은 다음과 같습니다.

IProxy proxyA = new A().Proxied();
IProxy proxyB = new B().Proxied();

귀하의 공장에 관해서는, 나는 당신이 IProxy 및 수행 그냥 통과 그것과 다른 방법의 모든 논리를 받아들이는 "주"공장 방법으로 리팩토링 할 수 있는지 것 new A().Proxied()또는 new B().Proxied():

public class LogicToBeApplied
{
    public A CreateA() {
      A a = new A();
      InitializeProxy(a.Proxied());
      return a; // or maybe return the proxy if you'd rather use that
    }

    public B CreateB() {
      B b = new B();
      InitializeProxy(b.Proxied());
      return b;
    }

    private void InitializeProxy(IProxy proxy)
    {
        proxy.IntProperty = 50;
    }
}

C ++ 템플릿은 구조적 타이핑에 의존하기 때문에 C #에서 C ++ 코드와 동등한 작업을 수행 할 방법이 없습니다 . 두 클래스가 동일한 메소드 이름과 서명을 갖는 한 C ++에서는 두 메소드에서 일반적으로 해당 메소드를 호출 할 수 있습니다. C #에는 공칭 타이핑 이 있습니다. 클래스 또는 인터페이스 의 이름 은 해당 유형의 일부입니다. 따라서, 명시 적 "is a"관계가 상속 또는 인터페이스 구현을 통해 정의되지 않는 한 클래스 AB용량을 동일하게 처리 할 수 ​​없습니다.

클래스별로 이러한 메소드를 구현하는 상용구가 너무 많은 경우 특정 특성 이름을 찾아서 오브젝트를 가져 와서 반사적으로 빌드하는 함수를 작성할 수 있습니다 LambdaProxy.

public class ReflectiveProxier 
{
    public object proxyReflectively(object proxied)
    {
        PropertyInfo prop = proxied.GetType().GetProperty("Property");
        return new LambdaProxy(
            () => prop.GetValue(proxied),
            (val) => prop.SetValue(proxied, val));
     }
}

잘못된 유형의 객체가 주어지면 이것은 아주 작습니다. 리플렉션은 본질적으로 C # 유형 시스템으로 방지 할 수없는 오류 가능성을 유발합니다. 다행히도 설탕의 추가를 위해 IProxy 인터페이스 또는 LambdaProxy 구현을 수정할 필요가 없으므로 헬퍼의 유지 관리 부담이 너무 커질 때까지 반사를 피할 수 있습니다.

이것이 작동하는 이유 중 일부 LambdaProxy는 "최대한 일반"입니다. LambdaProxy의 구현은 주어진 getter 및 setter 함수로 완전히 정의되므로 IProxy 계약의 "정신"을 구현하는 모든 값을 조정할 수 있습니다. 클래스의 속성 이름이 다르거 나 ints 로 현명하고 안전하게 표현 가능한 다른 유형 이거나 Property클래스의 다른 기능에 나타내는 개념을 매핑 할 수있는 방법이있는 경우에도 작동 합니다. 기능에서 제공하는 간접적 인 기능은 최대한의 유연성을 제공합니다.


매우 흥미로운 접근 방식이며 함수 호출에 확실히 사용될 수 있지만 실제로 ClassA 및 ClassB의 객체를 만들어야하는 팩토리에 사용할 수 있습니까?
블라디미르 스토 키

@VladimirStokic 수정 사항보기,이 부분을 조금 확장했습니다
Jack

분명히이 방법을 사용하려면 매핑 함수가 버그 인 경우 런타임 오류 가능성이 추가 된 각 유형의 속성을 명시 적으로 매핑해야합니다.
Ewan

의 대안으로 키워드를 ReflectiveProxier사용하여 프록시를 작성할 수 dynamic있습니까? 나에게 당신은 같은 근본적인 문제 (런타임시에만 잡히는 오류)가있을 것 같지만 구문과 유지 보수가 훨씬 간단합니다.
밥슨

1
@ 잭-충분합니다. 나는 그것을 내 자신의 대답을 추가 했다. 드문 상황 (예 : 이와 같은)에서 매우 유용한 기능입니다.
밥슨

12

다음은 기존 A 및 B 오브젝트에 어댑터를 사용할 수있는 A 및 / 또는 B에서 상속하지 않고 어댑터를 사용하는 방법에 대한 개요입니다.

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied<T> where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : IAdapter
{
    A _a;

    public AAdapter()  // use this if you want to have the "logic" part create new objects
    {
        _a=new A();
    }

    public AAdapter(A a) // if you need an adapter for an existing object afterwards
    {
       _a=a;
    }

    public int Property
    {
        get { return _a.Property; }
        set { _a.Property = value; }
    }

    public A {get{return _a; } } // to provide access for non-generic code
}

class BAdapter 
{
     // analogously
}

나는 일반적으로 클래스 프록시보다 이러한 종류의 객체 어댑터를 선호하며 상속으로 인해 발생할 수있는 추악한 문제를 피할 수 있습니다. 예를 들어이 솔루션은 A와 B가 봉인 된 클래스 인 경우에도 작동합니다.


new int Property? 당신은 아무것도 그림자하지 않습니다.
pinkfloydx33

@ pinkfloydx33 : 오타 일뿐입니다. 감사합니다.
Doc Brown

9

당신은 적응할 수 ClassAClassB공통 인터페이스를 통해. 이렇게하면 코드 LogicAToBeApplied가 동일하게 유지됩니다. 당신이 가진 것과 크게 다르지 않습니다.

class A
{
    public int Property { get; set; }
}
class B
{
    public int Property { get; set; }
}

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied<T> where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : A, IAdapter { }

class BAdapter : B, IAdapter { }

1
여기서는 어댑터 패턴을 사용한 +1 이 전통적인 OOP 솔루션입니다. A, B유형을 공통 인터페이스에 적용하기 때문에 프록시보다 더 많은 어댑터 입니다. 가장 큰 장점은 공통 논리를 복제 할 필요가 없다는 것입니다. 단점은 이제 논리가 실제 유형 대신 래퍼 / 프록시를 인스턴스화한다는 것입니다.
amon

5
이 솔루션의 문제점은 A 및 B 유형의 두 개체를 간단히 가져 와서 AProxy 및 BProxy로 변환 한 다음 LogicToBeApplied를 적용 할 수 없다는 것입니다. 이 문제는 상속 대신 집계를 사용하여 해결할 수 있습니다 (A 및 B에서 파생되는 것이 아니라 A 및 B 개체에 대한 참조를 통해 프록시 개체를 구현). 상속의 잘못된 사용이 어떻게 문제를 일으키는 지 다시 한 번 예.
Doc Brown

@DocBrown이 특별한 경우에는 어떻게됩니까?
블라디미르 스토 키

1
@ 잭 : 이러한 종류의 솔루션은 LogicToBeApplied특정 복잡성이 있을 때 의미 가 있으며 어떤 상황에서도 코드베이스의 두 곳에서 반복해서는 안됩니다. 그런 다음 추가 상용구 코드는 무시해도됩니다.
Doc Brown

1
@ 잭 중복은 어디에 있습니까? 두 클래스에는 공통 인터페이스가 없습니다. 당신은 래퍼 생성 공통의 인터페이스를 가지고 있습니다. 해당 공통 인터페이스를 사용하여 논리를 구현합니다. C ++ 코드에는 동일한 중복성이 존재하지 않습니다. 약간의 코드 생성 뒤에 숨겨져 있습니다. 당신이 강하게에 대한 것을 느끼는 경우에 보면 그들이 비록 동일 하지 않습니다 같은, 당신은 항상 T4s 또는 다른 템플릿 시스템을 사용할 수 있습니다.
Luaan

8

C ++ 버전은 템플릿이 "정적 오리 타이핑"을 사용하기 때문에 작동합니다. 유형이 올바른 이름을 제공하는 한 모든 것이 컴파일됩니다. 매크로 시스템과 비슷합니다. C #과 다른 언어의 제네릭 시스템은 매우 다르게 작동합니다.

devnull과 Doc Brown의 답변은 어댑터 패턴을 사용하여 알고리즘을 일반적인 상태로 유지하고 임의의 유형에서 작동하는 방법을 보여줍니다. 몇 가지 제한 사항이 있습니다. 특히, 실제로 원하는 것과 다른 유형을 만들고 있습니다.

약간의 속임수를 사용하면 변경하지 않고 원하는 유형을 정확하게 사용할 수 있습니다. 그러나 이제 대상 유형과의 모든 상호 작용을 별도의 인터페이스로 추출해야합니다. 여기에서 이러한 상호 작용은 구성 및 속성 할당입니다.

interface IInteractions<T> {
  T Instantiate();
  void AssignProperty(T target, int value);
}

OOP 해석에서 이는 전략 패턴 의 예입니다. 제네릭과 혼합되었지만 .

그런 다음 이러한 상호 작용을 사용하도록 논리를 다시 작성할 수 있습니다.

public class LogicBToBeApplied<T>
{
    public T CreateElement(IInteractions<T> interactions)
    {
        T retVal = interactions.Instantiate();
        interactions.AssignProperty(retVal, 50);
        return retVal;
    }
}

상호 작용 정의는 다음과 같습니다.

class Interactions_ClassA : IInteractions<ClassA> {
  public override ClassA Instantiate() { return new ClassA(); }
  public override void AssignProperty(ClassA target, int value) { target.IntProperty = value; }
}

이 방법의 가장 큰 단점은 프로그래머가 로직을 호출 할 때 상호 작용 인스턴스를 작성하고 전달해야한다는 것입니다. 이것은 어댑터 패턴 기반 솔루션과 상당히 유사하지만 약간 더 일반적입니다.

내 경험상, 이것은 다른 언어로 템플릿 기능을 얻을 수있는 가장 가까운 것입니다. Haskell, Scala, Go 및 Rust에서도 비슷한 기술을 사용하여 형식 정의 외부의 인터페이스를 구현합니다. 그러나 이러한 언어에서 컴파일러는 올바른 상호 작용 인스턴스를 암시 적으로 시작하고 선택하므로 실제로 추가 인수가 표시되지 않습니다. 이것은 C #의 확장 메소드와 유사하지만 정적 메소드로 제한되지 않습니다.


재미있는 접근법. 내 첫 번째 선택은 아니지만 프레임 워크 또는 이와 유사한 것을 작성할 때 이점이있을 수 있습니다.
Doc Brown

8

바람에주의를 기울이려면 "동적"을 사용하여 컴파일러가 모든 반사 불쾌감을 처리하도록 할 수 있습니다. SomeProperty라는 속성이없는 SetSomeProperty에 개체를 전달하면 런타임 오류가 발생합니다.

using System;

namespace ConsoleApplication3
{
    class A
    {
        public int SomeProperty { get; set; }
    }

    class B
    {
        public int SomeProperty { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var a = new A();
            var b = new B();

            SetSomeProperty(a, 7);
            SetSomeProperty(b, 12);

            Console.WriteLine($"a.SomeProperty = {a.SomeProperty}, b.SomeProperty = {b.SomeProperty}");
        }

        static void SetSomeProperty(dynamic obj, int value)
        {
            obj.SomeProperty = value;
        }
    }
}

4

다른 답변은 문제를 올바르게 식별하고 실행 가능한 솔루션을 제공합니다. C #은 (일반적으로) "오리 타자" ( "오리처럼 걷는 경우 ...")를 지원하지 않으므로 강제로 할 수있는 방법이 없습니다.ClassAClassB 그들은 길 있도록 설계되지 않은 경우 교환 할 수는.

그러나 이미 런타임 결함의 위험을 감수하고 싶다면 Reflection을 사용하는 것보다 쉬운 대답이 있습니다.

C #에는 dynamic이와 같은 상황에 적합한 키워드가 있습니다. "컴파일러가 런타임까지 (그리고 어쩌면 그때까지) 어떤 유형인지 알지 못하므로 그렇게 할 수 있습니다. 아무 것도 없다 "고 말한다.

이를 사용하여 원하는 기능을 정확하게 작성할 수 있습니다.

public class LogicToBeApplied<T> where T : new()
{
    public static T CreateElement()
    {
        dynamic retVal = new T(); // This doesn't care what type T is.
        retVal.IntProperty = 50;  // This will fail at runtime if there is no "IntProperty" 
                                  // or it doesn't accept an int.
        return retVal;            // Once again, we don't care what it is.
    }
}

static키워드 사용에도 유의하십시오 . 이를 통해 다음과 같이 사용할 수 있습니다.

A classAElement = LogicToBeApplied<A>.CreateElement();
B classBElement = LogicToBeApplied<B>.CreateElement();

dynamic리플렉션 사용의 일회성 히트 (및 복잡성 추가)와 같이을 사용 하면 성능에 큰 영향을 미치지 않습니다 . 코드가 특정 유형의 동적 호출에 처음으로 도달하면 약간의 오버 헤드 가 발생하지만 반복되는 호출은 표준 코드만큼 빠릅니다. 그러나, 당신은 것입니다 를 얻을 RuntimeBinderException당신이 그 속성이 없습니다 뭔가를 전달하려고하면, 시간의 전방을 확인하는 좋은 방법은 없습니다. 유용한 방법으로 해당 오류를 구체적으로 처리 할 수 ​​있습니다.


느려질 수 있지만 느린 코드는 문제가되지 않습니다.
Ian

@Ian-좋은 지적. 성능에 대해 조금 더 추가했습니다. 같은 장소에서 같은 클래스를 재사용한다면 실제로 생각보다 나쁘지 않습니다.
밥슨

C ++ 템플릿에서는 가상 메서드의 오버 헤드도 없습니다!
Ian

2

리플렉션을 사용하여 이름으로 속성을 가져올 수 있습니다.

public class logic 
{
    public object getNew<T>() where T : new()
    {
        T ret = new T();
        try
        {
            var property = typeof(T).GetProperty("IntProperty");
            if (property != null && property.PropertyType == typeof(int))
            {
                property.SetValue(ret, 50);
            }
        }
        catch (AmbiguousMatchException)
        {
            //hmm..
        }
        return ret;
    }
}

분명히이 방법으로 런타임 오류가 발생할 위험이 있습니다. C #이 당신을 멈추려 고하는 것입니다.

미래의 C # 버전에서는 개체를 상속하지는 않지만 일치하는 인터페이스로 개체를 전달할 수 있다는 내용을 읽었습니다. 어느 쪽도 문제를 해결할 것입니다.

(나는 기사를 파헤쳐 볼 것이다)

또 다른 방법은 코드를 절약 할 수는 없지만 A와 B를 서브 클래스 화하고 IntProperty로 인터페이스를 상속하는 것입니다.

public interface IIntProp {
    public int IntProperty {get, set}
}

public class A2 : A, IIntProp {}

public class B2 : B, IIntProp {}

런타임 오류 가능성과 성능 문제는 내가 반성하고 싶지 않은 이유입니다. 그러나 귀하의 답변에 언급 한 기사를 읽는 데 관심이 있습니다. 그것을 읽고 기대합니다.
블라디미르 스토 키

1
반드시 C ++ 솔루션과 동일한 위험을 감수하고 있습니까?
Ewan

4
@Ewan no, c ++는 컴파일 타임에 멤버를 확인합니다
Caleth

리플렉션은 최적화 문제와 런타임 오류를 디버깅하기가 훨씬 더 어렵다는 것을 의미합니다. 상속 및 공통 인터페이스는 이러한 클래스 중 하나 하나에 대해 서브 클래스를 미리 선언하고 (명명하게 익명으로 만들 수 없음) 매번 동일한 속성 이름을 사용하지 않으면 작동하지 않습니다.
Jack

1
@Jack은 단점이 있지만 매퍼, 시리얼 라이저, 의존성 주입 프레임 워크 등에서 리플렉션이 광범위하게 사용되고 목표는 최소한의 코드 복제로이를 수행하는 것입니다.
Ewan

0

implicit operator잭의 답변에 대한 대의원 / 람다 접근과 함께 변환 을 사용하고 싶었습니다 . AB같은 가정 :

// A and B are mutable reference types

class A
{
  public int IntProperty { get; set; }
}

class B
{
  public int IntProperty { get; set; }
}

그런 다음 암시 적 사용자 정의 변환 (확장 메서드 또는 이와 유사한 필요 없음)을 사용하여 유용한 구문을 쉽게 얻을 수 있습니다.

// Adapter is an immutable type. However, the delegate instances have a captured reference to an A or a B (closure semantics)
struct Adapter
{
  readonly Func<int> getter;
  readonly Action<int> setter;

  Adapter(Func<int> getter, Action<int> setter)
  {
    this.getter = getter;
    this.setter = setter;
  }

  public int IntProperty
  {
    get { return getter(); }
    set { setter(value); }
  }

  public static implicit operator Adapter(A a) => new Adapter(() => a.IntProperty, x => a.IntProperty = x);
  public static implicit operator Adapter(B b) => new Adapter(() => b.IntProperty, x => b.IntProperty = x);

  public A CloneToA() => new A { IntProperty = getter(), };
  public B CloneToB() => new B { IntProperty = getter(), };
}

사용 예시 :

class LogicToBeApplied
{
  public static A CreateA()
  {
    var a = new A();
    Initialize(a);
    return a;
  }
  public static B CreateB()
  {
    var b = new B();
    Initialize(b);
    return b;
  }

  static void Initialize(Adapter a)
  {
    a.IntProperty = 50;
  }
}

Initialize함께 작업 할 수있는 방법 방법을 보여줍니다 Adapter그것이 여부에 대해 걱정하지 않고 AB또는 뭔가. 의의 호출이 Initialize방법 쇼 우리는 (표시) 캐스트 또는 필요가 없습니다 .AsProxy()치료 콘크리트와 유사하거나 A또는 B등을Adapter .

ArgumentNullException전달 된 인수가 널 참조인지 여부에 따라 사용자 정의 변환 을 처리 할 것인지 고려하십시오 .

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