C # Generics - Gereksiz metodlardan nasıl kaçınılır?


28

Buna benzeyen iki sınıfım olduğunu varsayalım (ilk kod bloğu ve genel sorun C # ile ilgilidir):

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

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

Bu sınıflar hiçbir şekilde değiştirilemez (3. parti meclisinin bir parçasıdır). Bu nedenle, aynı arayüzü kurmalarını ya da IntProperty içerecek olan aynı sınıfı devralmalarını sağlayamıyorum.

Her IntPropertyiki sınıfın da özelliklerine bir miktar mantık uygulamak istiyorum ve C ++ 'da bunu kolayca yapmak için bir şablon sınıfı kullanabilirim:

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

};

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

Sonra böyle bir şey yapabilirim:

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

Bu şekilde hem ClassA hem de ClassB için çalışacak tek bir genel fabrika sınıfı oluşturabilirim.

Ancak, C #, wheremantık için kod tamamen aynı olmasına rağmen iki farklı cümlecik ile iki sınıf yazmak zorundayım :

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

Biliyorum ki eğer farklı sınıflarda ders almak istersem whereMaddede , birbirleriyle ilgili olmaları gerektiğini, yani aynı sınıfa miras almaları gerektiğini, eğer aynı kodu yukarıda anlattığım şekilde uygulamak istediğimi biliyorum. Sadece iki tamamen aynı yöntemi olması çok can sıkıcı bir durum. Ayrıca performans sorunları nedeniyle yansıma kullanmak istemiyorum.

Birisi bunun daha şık bir tarzda yazılabileceği bir yaklaşım önerebilir mi?


3
Neden bunun için jenerik kullanıyorsunuz? Bu iki işlev hakkında genel bir şey yok.
Luaan

1
@Luaan Bu, soyut fabrika modelinin bir varyasyonunun basitleştirilmiş bir örneğidir. ClassA veya ClassB'yi devralan düzinelerce sınıf olduğunu ve ClassA ve ClassB'nin soyut sınıflar olduğunu hayal edin. Miras alınan sınıflar ek bilgi içermez ve bunların örneklendirilmesi gerekir. Her biri için bir fabrika yazmak yerine, jenerik ilaçları kullanmayı seçtim.
Vladimir Stokic

6
Peki, gelecek sürümlerde kırmayacağına eminseniz, yansıma veya dinamikleri kullanabilirsiniz.
Casey,

Aslında bu, jenerik ürünler hakkındaki en büyük şikayetim bunu yapamaması.
Joshua

1
@Joshua, ben daha "ördek yazmayı" desteklemeyen arayüzlerle ilgili bir sorun olarak düşünüyorum.
Ian

Yanıtlar:


49

Proxy arayüzü ekleyin (bazen bir adaptör de denir , bazen çok küçük farklar vardır), LogicToBeAppliedproxy açısından uygulayın , ardından bu proxy'nin bir örneğini iki lambdadan oluşturmanın bir yolunu ekleyin: biri özellik için diğeri set için.

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

Artık ne zaman bir IProxy’ye geçmeniz gerekiyorsa, ancak üçüncü taraf sınıflarının bir örneğine sahipseniz, sadece bazı lambdaları geçebilirsiniz:

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

Ek olarak, A veya B örneklerinden LamdaProxy örneklerini oluşturmak için basit yardımcılar yazabilirsiniz. Hatta size "akıcı" bir stil vermek için uzatma yöntemleri de olabilir:

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

Ve şimdi proxy inşaatı şuna benziyor:

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

Fabrikanıza gelince, bir IProxy ve gerçekleştirir sadece geçmek o ve diğer yöntemlerle tüm mantık kabul eden bir "ana" fabrika yöntemiyle içine planı ayrı görmek istiyorum new A().Proxied()ya 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 ++ kodunuzun eşdeğerini C # ile yapmanın bir yolu yoktur, çünkü C ++ şablonları yapısal yazmaya dayanır . İki sınıf aynı yöntem adına ve imzasına sahip olduğu sürece, C ++ 'da bu yöntemi her ikisinde de genel olarak çağırabilirsiniz. C # 'nin nominal yazması vardır - bir sınıfın veya arayüzün adı türünün bir parçasıdır. Bu nedenle, sınıflar Ave Bherhangi bir kapasitede aynı şekilde muamele edilemez, aksi takdirde "bir" ilişki miras veya arayüz uygulaması yoluyla tanımlanır.

Sınıf başına bu yöntemleri uygulama kazanı çok fazlaysa, bir nesneyi alan ve belirli bir özellik adını arayarak yansıtıcı bir şekilde oluşturulan bir işlev yazabilirsiniz 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));
     }
}

Yanlış türde nesneler verildiğinde bu aşırı derecede başarısız olur; yansıma doğal olarak C # tipi sistemin önleyemediği arıza olasılığını ortaya koymaktadır. Neyse ki, yardımcıların bakım yükü çok büyük oluncaya kadar yansımayı önleyebilirsiniz çünkü yansıtıcı şekeri eklemek için IProxy arayüzünü veya LambdaProxy uygulamasını değiştirmeniz gerekmez.

Bunun işe LambdaProxyyaramasının bir nedeni de "azami derecede genel" olması; IPdaxy sözleşmesinin "ruhunu" uygulayan herhangi bir değeri uyarlayabilir, çünkü LambdaProxy'nin uygulanması, verilen alıcı ve ayarlayıcı işlevleriyle tamamen tanımlanır. Sınıflar özellik için farklı isimlere veya ints olarak algılanabilir ve güvenli bir şekilde temsil edilebilecek farklı tiplere sahipse ya Propertyda sınıfın diğer özelliklerini temsil etmesi gereken kavramı eşlemenin bir yolu varsa bile çalışır . Fonksiyonların sağladığı dolaylı kullanım size maksimum esneklik sağlar.


Çok ilginç bir yaklaşım ve işlevleri çağırmak için kesinlikle kullanılabilir, ancak aslında ClassA ve ClassB nesnelerini oluşturmam gereken fabrika için kullanılabilir mi?
Vladimir Stokic,

@VladimirStokic Düzenlemeleri görün, bu konuyu biraz büyüttüm
Jack

elbette bu yöntem hala, eşleme işleviniz buggy ise, çalışma zamanı hatası eklenmiş olasılığı olan her tür için özelliği açıkça eşlemenizi gerektirir
Ewan

Alternatif olarak ReflectiveProxier, dynamicanahtar kelimeyi kullanarak bir proxy oluşturabilir misiniz? Bana göre aynı temel sorunlara sahip olacaktınız (yani sadece çalışma zamanında yakalanan hatalar), ancak sözdizimi ve bakım kolaylığı çok daha basit olurdu.
Bobson,

1
@Jack - Yeterince adil. Eklediğim kendi cevap onu demoing. Bazı nadir durumlarda (bunun gibi) çok kullanışlı bir özellik.
Bobson

12

A ve / veya B'den miras almadan bağdaştırıcıların, mevcut A ve B nesneleri için kullanma olasılıkları ile nasıl kullanılacağının ana hatları aşağıda verilmiştir:

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
}

Genelde bu tür bir nesne bağdaştırıcısını sınıf proxy'lerine tercih ederim, mirasa maruz kalabileceğiniz çirkin sorunlardan kaçınırlar. Örneğin, bu çözüm A ve B mühürlü sınıflar olsa bile işe yarayacaktır.


Neden new int Property? Hiçbir şeyi gölgelendirmiyorsun.
pinkfloydx33

@ pinkfloydx33: Sadece bir yazım hatası, değiştirdim, teşekkürler.
Doktor Brown,

9

Ortak bir arayüz üzerinden ClassAve adapte olabilir ClassB. Bu şekilde kodunuz LogicAToBeAppliedaynı kalır. Sahip olduklarından çok farklı değil.

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
Adaptör Örüntüsünü kullanarak +1 buradaki geleneksel OOP çözümüdür. Biz uyum beri daha bir vekil daha bir adaptörün var A, Bortak bir arayüz türleri. En büyük avantaj, ortak mantığı çoğaltmamız gerekmemesidir. Dezavantajı, mantığın şimdi gerçek tür yerine sarıcı / proxy'yi başlatmasıdır.
amon

5
Bu çözümle ilgili sorun, A ve B türündeki iki nesneyi basitçe alamamanız, bunları bir şekilde AProxy ve BProxy'ye dönüştürmeniz ve daha sonra bunlara LogicToBeApplied uygulamanızdır. Bu sorun, kalıtım yerine birleştirme kullanılarak çözülebilir (sırasıyla, proxy nesnelerini A ve B'den değil, A ve B nesnelerine referans alarak uygulayın). Yine mirasın nasıl yanlış kullanıldığına bir örnek sorun yaratır.
Doktor Brown,

@DocBrown Bu, bu durumda nasıl olurdu?
Vladimir Stokic,

1
@Jack: bu tür bir çözüm, LogicToBeAppliedbelirli bir karmaşıklığa sahip olduğunda anlamlıdır ve hiçbir koşulda kod tabanında iki yerde tekrarlanmamalıdır. Sonra ek boyler kodu çoğu zaman ihmal edilebilir.
Doktor Brown

1
@Jack Artıklık nerede? İki sınıfın ortak bir arayüzü yok. Sen mahfazalar oluşturmak do ortak arayüze sahip. Mantığınızı uygulamak için bu ortak arayüzü kullanın. Aynı fazlalık C ++ kodunda mevcut değil gibi değil - sadece biraz kod oluşturma arkasında gizli. Bunu şiddetle ilgili şeyler hissediyorsan bakmak onlar bile, aynı değildir , aynı, her zaman T4S veya başka şablon sistemini kullanabilirsiniz.
Luaan

8

C ++ sürümü yalnızca şablonları “statik ördek yazmayı” kullandığı için çalışır - tür doğru adlar sağladığı sürece her şey derlenir. Makro sistemi gibi daha fazla. C # ve diğer dillerin jenerik sistemi çok farklı çalışır.

devnull's ve Doc Brown'ın cevapları, bağdaştırıcı deseninin algoritmanızı genel tutmak için nasıl kullanılabileceğini ve yine de bazı kısıtlamalarla keyfi türlerde çalışabileceğini gösteriyor. Özellikle, şimdi gerçekten istediğinden farklı bir tür yaratıyorsun.

Biraz kandırmayla, amaçlanan türü herhangi bir değişiklik yapmadan kullanmak mümkündür. Ancak, şimdi hedef tür ile tüm etkileşimleri ayrı bir arayüze çıkarmamız gerekiyor. Burada, bu etkileşimler inşaat ve mülk atamasıdır:

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

Bir OOP yorumunda, bu strateji modelinin bir örneği olacaktır. , jeneriklerle karıştırılsa da .

Bu etkileşimleri kullanmak için mantığınızı yeniden yazabiliriz:

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

Etkileşim tanımları şöyle görünür:

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

Bu yaklaşımın en büyük dezavantajı, programcının mantığı çağırırken bir etkileşim örneği yazması ve aktarması gerektiğidir. Bu, adaptör deseni tabanlı çözümlere oldukça benzer, ancak biraz daha geneldir.

Tecrübelerime göre, şablon fonksiyonlarını diğer dillerde bulabileceğiniz en yakın şey bu. Benzer teknikler, Haskell, Scala, Go ve Rust'ta, tür tanımının dışındaki arabirimleri uygulamak için kullanılır. Bununla birlikte, bu dillerde derleyici dolaylı olarak doğru etkileşim örneğine girer ve seçer, böylece fazladan bir argüman görmezsiniz. Bu, C # 'nın uzatma yöntemlerine de benzer, ancak statik yöntemlerle sınırlı değildir.


İlginç yaklaşım Benim ilk tercihim olan değil, ama sanırım bir çerçeve ya da onun gibi bir şey yazarken bazı avantajları olabilir.
Doktor Brown,

8

Eğer gerçekten rüzgara dikkat etmek istiyorsanız, derleyicinin sizin için bütün yansıma boşluğuna dikkat etmesi için "dinamik" i kullanabilirsiniz. Bu, SomeProperty adında bir özelliğe sahip olmayan bir SetSomeProperty nesnesine iletirseniz çalışma zamanı hatasıyla sonuçlanır.

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

Diğer cevaplar problemi doğru şekilde tespit eder ve uygulanabilir çözümler sunar. C # (genellikle) "ördek yazmayı" ("ördek gibi yürüyorsa ...") desteklemiyordur, bu yüzden tasarlanmamışlarsa, sizi zorlamanın ClassAve ClassBbirbirinin yerine geçmenin yolu yoktur.

Ancak, zaten bir çalışma zamanı hatası riskini kabul etmeye istekli iseniz, o zaman Reflection'ı kullanmaktan daha kolay bir cevap var.

C #, bu dynamicgibi durumlar için mükemmel olan bir anahtar kelimeye sahiptir. Derleyiciye “Bunun çalışma zamanına kadar ne tür olduğunu bilmeyeceğim (ve belki de o zaman bile değil, bu yüzden bana bir şey yapmama izin ver) .

Bunu kullanarak, istediğiniz işlevi tam olarak oluşturabilirsiniz:

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

staticAnahtar kelimenin kullanımına da dikkat edin . Bu, bunu kullanmanıza izin verir:

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

dynamicYansıma kullanmanın tek seferlik isabetinin (ve daha da karmaşıklığı) nasıl kullanılacağına ilişkin büyük bir resim performansı etkisi yoktur . Kodunuz, belirli bir türdeki dinamik aramaya ilk kez ulaştığında, az miktarda ek yüke sahip olur , ancak tekrarlanan çağrılar, standart kod kadar hızlı olur. Ancak, olacak bir olsun RuntimeBinderExceptiono özelliği olmayan bir şeyin geçmek çalışırsanız ve zamanın o yeşil ışık kontrol etmek iyi bir yolu yoktur. Bu hatayı özellikle faydalı bir şekilde ele almak isteyebilirsiniz.


Bu yavaş olabilir, ancak genellikle yavaş kod bir sorun değildir.
Ian

@Ian - İyi nokta. Performans hakkında biraz daha ekledim. Aynı noktalarda aynı sınıfları yeniden kullanmanız şartıyla, aslında sandığınız kadar kötü değil.
Bobson

C ++ şablonlarında sanal yöntemlerin ek yükü bulunmadığını unutmayın!
Ian

2

Yansıması, özelliği adıyla çıkarmak için kullanabilirsiniz.

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

Açıkçası, bu yöntemle çalışma zamanı hatası riskini alıyorsunuz. C # seni durdurmaya çalışıyor.

Gelecekteki C # sürümünün nesneleri miras almayan ama eşleştiren bir arabirim olarak geçirmenize izin vereceğini bir yerde okudum. Bu da senin problemini çözerdi.

(Makaleyi kazmaya çalışacağım)

Başka bir yöntem, size herhangi bir kod kaydettiğinden emin olmasam da, hem A hem de B'yi alt sınıflara sokmak ve ayrıca IntProperty ile bir Arayüz devralmak olacaktır.

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

public class A2 : A, IIntProp {}

public class B2 : B, IIntProp {}

Çalışma zamanı hatası ve performans sorunları olasılığı, yansıma ile gitmek istemediğim nedenler. Bununla birlikte, cevabınızda bahsettiğiniz makaleyi okumakla çok ilgileniyorum. Okumak için bekliyorum.
Vladimir Stokic

1
elbette c ++ çözümünüzle aynı riski alıyorsunuz?
Ewan

4
@Ewan no, c ++ derleme zamanında üyeyi kontrol eder
Caleth

Yansıma, optimizasyon sorunları ve (çok daha önemlisi) çalışma zamanı hatalarında hata ayıklamak zor anlamına gelir. Kalıtım ve ortak bir arayüz, bu sınıfların her biri için vaktinden önce bir alt sınıf ilan etmek anlamına gelir (anonim olarak adsız bir şekilde yapmanın bir yolu yoktur) ve her seferinde aynı özellik adını kullanmazlarsa çalışmaz.
Jack

1
@Jack dezavantajları vardır, ancak yansımanın Haritacılar, Serileştiriciler, Bağımlılık Enjeksiyonu çerçeveleri vb. Alanlarda yaygın bir şekilde kullanıldığını ve amacın en az kod çoğaltmasıyla yapmak olduğunu düşünün
Ewan

0

Sadece implicit operatordönüşümleri Jack'in cevabının delege / lambda yaklaşımı ile birlikte kullanmak istedim . Ave Bvarsayıldığı gibi:

// A and B are mutable reference types

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

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

Daha sonra, örtük kullanıcı tanımlı dönüşümlerle güzel sözdizimi elde etmek kolaydır (uzantı yöntemi veya benzeri bir şey gerekmez):

// 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(), };
}

Kullanımın gösterimi:

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

InitializeBirlikte çalışma nasıl bir yöntem gösterir Adapterbir olup olmadığı konusunda endişe etmeden Aveya bir Bveya başka bir şey. InitializeYöntemin çağrıları, .AsProxy()beton Aveya bir Bgibi muamele etmek için (görünür) herhangi bir alçıya veya benzerine ihtiyacımız olmadığını göstermektedir Adapter.

İletilen ArgumentNullExceptionargüman boş bir başvuru ise, kullanıcı tanımlı dönüşümlerde atmak isteyip istemediğinizi düşünün .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.