C#Generics-冗長な方法を避ける方法


28

次のような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#ではwhere、ロジックのコードがまったく同じであっても、2つの異なる句を持つ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つの方法があるのは非常に迷惑です。また、パフォーマンスの問題があるため、リフレクションを使用したくありません。

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


3
そもそもなぜこれにジェネリックを使用しているのですか?これらの2つの関数について一般的なものはありません。
ルアーン

1
@Luaanこれは、抽象ファクトリパターンのバリエーションの簡略化された例です。ClassAまたはClassBを継承するクラスが多数あり、ClassAとClassBが抽象クラスであると想像してください。継承されたクラスには追加情報はなく、インスタンス化する必要があります。それぞれのファクトリを作成する代わりに、ジェネリックを使用することにしました。
ウラジミールストーク

6
将来のリリースでリフレクションやダイナミクスが破壊されないことを確信している場合は、リフレクションまたはダイナミクスを使用できます。
ケーシー

これは実際、ジェネリックに関する私の最大の不満は、これができないということです。
ジョシュア

1
@ジョシュア、私はそれを「ダックタイピング」をサポートしていないインターフェースの問題だと思います。
イアン

回答:


49

プロキシインターフェース(アダプターと呼ばれることもありますが、微妙な違いがあります)LogicToBeAppliedを追加し、プロキシの観点から実装してから、2つのラムダからプロパティのインスタンスを作成する方法を追加します。

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インスタンスを構築する単純なヘルパーを作成できます。これらは、「流fluentな」スタイルを提供する拡張メソッドになることもできます。

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 ++コードに相当する方法はありません。2つのクラスのメソッド名とシグネチャが同じである限り、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));
     }
}

不正なタイプのオブジェクトを指定すると、これはabysmallyに失敗します。リフレクションは本質的に、C#型システムでは防止できない障害の可能性をもたらします。幸運なことに、IProxyインターフェースやLambdaProxy実装を変更してリフレクションシュガーを追加する必要がないため、ヘルパーのメンテナンスの負担が大きくなりすぎるまでリフレクションを回避できます。

これが機能する理由の一部は、LambdaProxy「最大限に汎用的」であることです。LambdaProxyの実装は、指定されたgetterおよびsetter関数によって完全に定義されるため、IProxyコントラクトの「精神」を実装する任意の値を適応させることができます。クラスがプロパティの異なる名前を持っている場合、またはints として賢明かつ安全に表現できる異なるタイプがある場合、またはPropertyクラスの他の機能に表現するはずの概念をマップする方法がある場合でも機能します。関数によって提供される間接化により、最大限の柔軟性が得られます。


非常に興味深いアプローチであり、関数の呼び出しには間違いなく使用できますが、実際にClassAおよびClassBのオブジェクトを作成する必要があるファクトリーに使用できますか?
ウラジミールストッキック

@VladimirStokic編集を参照して、これについて少し拡張しました
ジャック

確かにこのメソッドでは、マッピング関数にバグがある場合は、ランタイムエラーの可能性を追加して、各タイプのプロパティを明示的にマッピングする必要があります
Ewan

の代わりにReflectiveProxierdynamicキーワードを使用してプロキシを構築できますか?あなたは同じ基本的な問題(つまり、実行時にのみキャッチされるエラー)を抱えているように思えますが、構文と保守性ははるかに簡単でしょう。
ボブソン

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
}

私は通常、クラスプロキシよりもこの種のオブジェクトアダプタを好むでしょう。継承によって実行されるcanい問題を回避します。たとえば、AとBが封印されたクラスであっても、このソリューションは機能します。


なんでnew int Property?あなたは何も隠していません。
pinkfloydx33

@ pinkfloydx33:ただのタイプミス、それを変えた、ありがとう。
ドックブラウン

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ソリューションです。我々は適応するので、それは複数のプロキシよりも、アダプタのだAB共通のインターフェイスに種類を。大きな利点は、共通ロジックを複製する必要がないことです。欠点は、ロジックが実際の型ではなくラッパー/プロキシをインスタンス化することです。
アモン

5
このソリューションの問題は、タイプAとBの2つのオブジェクトを単純に取得し、それらを何らかの方法でAProxyとBProxyに変換してから、それらにLogicToBeAppliedを適用できないことです。この問題は、継承の代わりに集約を使用することで解決できます(AおよびBから派生するのではなく、AおよびBオブジェクトへの参照を取得することにより、プロキシオブジェクトを実装します)。また、継承の誤った使用が問題を引き起こす例。
ドックブラウン

@DocBrownこの特定のケースではどのようになりますか?
ウラジミールストッキック

1
@Jack:この種のソリューションLogicToBeAppliedは、特定の複雑さがある場合に意味があり、いかなる状況でもコードベースの2箇所で繰り返さないでください。その後、追加の定型コードはほとんど無視されます。
ドックブラウン

1
@Jack冗長性はどこにありますか?2つのクラスには共通のインターフェースはありません。あなたは、ラッパーを作成します共通のインタフェースを持っています。その共通インターフェースを使用して、ロジックを実装します。同じ冗長性がC ++コードに存在しないというわけではありません-ちょっとしたコード生成の背後に隠れているだけです。同じではないものの、同じように見えるものについて強く感じている場合、T4または他のテンプレートシステムをいつでも使用できます。
ルアーン

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


興味深いアプローチ。私の最初の選択肢となるものではありませんが、フレームワークなどを作成するときにいくつかの利点があると思います。
ドックブラウン

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#が(一般的に)をサポートしていません「ダックタイピング」(「それがアヒルのように歩きなら...」)ので、あなたを強制する方法はありませんClassAし、ClassB彼らはそのように設計されていなかった場合は交換可能であるが。

ただし、ランタイムフォールトのリスクを既に受け入れている場合は、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();

dynamicReflectionを使用することによるパフォーマンス全体への影響はありません。これは、Reflectionを使用する1回限りのヒット(および複雑さの増加)がある方法です。コードが初めて特定のタイプの動的呼び出しにヒットするときのオーバーヘッドはわずかですが、繰り返し呼び出しは標準コードと同じくらい高速です。しかし、あなたがなり得るRuntimeBinderException、あなたがそのプロパティを持っていない何かに合格しようとすると、時間のその先を確認する良い方法はありません場合。そのエラーを便利な方法で具体的に処理したい場合があります。


これは遅くなる可能性がありますが、多くの場合、遅いコードでも問題はありません。
イアン

@Ian-良い点。パフォーマンスについてもう少し追加しました。同じ場所で同じクラスを再利用するのであれば、実際にあなたが思うほど悪くはありません。
ボブソン

C ++テンプレートでは、仮想メソッドのオーバーヘッドさえも持たないことを忘れないでください!
イアン

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 ++ソリューションでも同じリスクを負いますか?
ユアン

4
@Ewanいいえ、c ++はコンパイル時にメンバーをチェックします
Caleth

リフレクションとは、最適化の問題と、(さらに重要なことですが)ランタイムエラーのデバッグが難しいことを意味します。継承と共通インターフェースは、これらのクラスのすべてのクラスに対して事前にサブクラスを宣言することを意味し(その場で匿名で作成する方法はありません)、毎回同じプロパティ名を使用しないと機能しません。
ジャック

1
@Jack欠点があるが、その反射がマッパー、シリアライザ、依存性注入フレームワークなどで、ゴールは、コードの重複の最小量でそれを行うことであると広く使用されて考える
ユアン・

0

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; }
}

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

// 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あるかどうかを気にせずに作業する方法を示します。このメソッドの呼び出しは、コンクリートを処理するために、またはABInitialize.AsProxy()ABAdapter

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.