web-dev-qa-db-ja.com

C#Generics-冗長なメソッドを回避する方法は?

次のような2つのクラスがあるとしましょう(コードの最初のブロックと一般的な問題は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#では、ロジックのコードがまったく同じであっても、2つの異なるwhere句を使用して2つのクラスを作成する必要があります。

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句に異なるクラスを含める場合は、それらを関連付ける必要があります。つまり、上記の意味で同じコードを適用する場合は、同じクラスを継承する必要があります。 。 2つの完全に同じメソッドを使用するのが非常に煩わしいというだけです。また、パフォーマンスの問題のため、リフレクションを使用したくありません。

誰かがこれをよりエレガントな方法で書くことができるいくつかのアプローチを提案できますか?

28
Vladimir Stokic

プロキシインターフェイス(アダプタと呼ばれることもあり、場合によっては微妙な違いがある)を追加し、プロキシの観点からLogicToBeAppliedを実装してから、 2つのラムダからこのプロキシのインスタンスを作成する方法:1つはプロパティget用、もう1つはセット用です。

_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++テンプレートは structural タイピングに依存しているため、C#でC++コードと同等のことを行う方法はありません。 2つのクラスが同じメソッド名とシグネチャを持っている限り、C++では、そのメソッドを両方のクラスで総称的に呼び出すことができます。 C#には 公称 タイピングがあります-クラスまたはインターフェースのnameはそのタイプの一部です。したがって、クラスABは、継承またはインターフェイスの実装のいずれかで明示的な「is a」関係が定義されていない限り、どの容量でも同じに扱うことはできません。

クラスごとにこれらのメソッドを実装するボイラープレートが多すぎる場合は、オブジェクトを取り、reflectivelyLambdaProxyを構築する関数を作成できます特定のプロパティ名を探すことにより:

_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の実装は特定のゲッター関数とセッター関数によって完全に定義されるため、IProxyコントラクトの「精神」を実装する任意の値を適合させることができます。クラスのプロパティの名前が異なる場合、またはintsとして適切かつ安全に表現できる異なるタイプの場合、またはPropertyが表すことになっている概念をマッピングする方法がある場合でも機能しますクラスの他の機能に。関数によって提供される間接参照は、最大限の柔軟性を提供します。

49
Jack

以下は、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が封印されたクラスであっても機能します。

12
Doc Brown

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 { }
9
devnull

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#の拡張メソッドに似ていますが、静的メソッドに限定されません。

8
amon

本当に風に注意を払いたい場合は、「動的」を使用して、コンパイラーにすべてのリフレクションの不快さを処理させることができます。これにより、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;
        }
    }
}
8
OldFart

他の答えは問題を正しく識別し、実行可能な解決策を提供します。 C#は(通常) "duck入力" ( "カモのように歩く場合...")をサポートしていないため、ClassAClassBそれらがそのように設計されていない場合、互換性があるようにします。

ただし、ランタイムフォールトのリスクを受け入れる用意がある場合は、リフレクションを使用するよりも簡単な答えがあります。

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の使用による全体像のパフォーマンスへの影響はありません。これは、Reflectionを使用する1回限りのヒット(および追加の複雑さ)の方法です。コードが最初に特定のタイプの動的呼び出しにヒットしたとき オーバーヘッドはわずかになります ですが、繰り返しの呼び出しは標準コードと同じくらい高速です。ただし、そのプロパティを持たないものを渡そうとすると、willRuntimeBinderExceptionを取得します。これを事前に確認する良い方法はありません。そのエラーを便利な方法で具体的に処理したい場合があります。

4
Bobson

リフレクションを使用して、プロパティを名前で取得できます。

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 {}
2
Ewan

implicit operator変換と、ジャックの回答のデリゲート/ラムダアプローチ。 AおよびBは想定どおりです。

// A and B are mutable reference types

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

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

次に、暗黙的なユーザー定義の変換を使用して、Nice構文を簡単に取得できます(拡張メソッドなどは必要ありません)。

// 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であるかAであるかを気にせずにBを使用する方法を示しています。 Initializeメソッドの呼び出しは、具体的なAまたはBAdapterとして扱うために、(可視)キャストや.AsProxy()などが不要であることを示しています。

渡された引数がnull参照であるかどうかにかかわらず、ユーザー定義の変換でArgumentNullExceptionをスローするかどうかを検討してください。

0