簡體   English   中英

如何實現 C# 通用 class 其方法在結構上是通用的?

[英]How to implement a C# generic class whose methods are generic on structs?

假設我們有這對結構,它是廣泛使用的交換格式的一部分(所以我們不能修改源 - 理想情況下不應該這樣做:我們不是試圖改變數據本身):

struct Vector2 { public int x; public int y; }
struct Vector3 { public int x; public int y; public int z; }

還有一個 class ,其核心是一個或另一個列表,並包含許多算法,這些算法對於任一結構實現幾乎相同,但必須引用 3 元素結構( z )中的額外成員:

public class Mesh<T>
{
    private List<T> _myVectors;
}

...您如何正確實現處理它們的方法套件? 例如:

public int Average()
{
    // if Vector2:
    int sum = 0, count = 0;
    foreach( var v2 in _MyVectors )
    {
        sum += v2.x + v2.y;
        count++;
    }

    // if Vector3:
    int sum = 0, count = 0;
    foreach( var v3 in _MyVectors )
    {
        sum += v3.x + v3.y + v3.z;
        count++;
    }

    return sum/count;
}

特別注意:結構特定的方法已經存在(數百萬個),由缺少任何內置 Generics 的 API 提供。 因此,例如,我們可以自信地編寫一個算法來使用FOREIGN_API中的方法,因為知道源代碼的一個副本(但使用泛型)將以任何一種方式綁定到可接受的實現:

public float FOREIGN_API_Average( Vector2 input );
public float FOREIGN_API_Average( Vector3 input );

我試圖在這里解決的問題大約是:

  1. 這是上面示例中的// if Vector2:概念部分,我無法弄清楚如何在 C# 中執行。
  2. 我只是不確定如何構建它。 感覺就像我必須做一些巧妙的技巧來說明“我有任意 generics 參數。但我有這個 class 的一些特定版本,我將在內部實現。(所有其他非特定版本暗示非法,我會實現一組拋出異常的方法)”。 但是...我不知道如何解決這個問題。
  3. 結構不能相互“擴展”。 如果它們是類,我將從使用where T: BaseVector和 go 約束使用類(Mesh)開始。 但這對於結構是不可能的。
  4. 這些結構來自我不擁有的(十億美元)軟件; 有很多架構決策我希望他們做出不同的決定,但是 TL;DR:必須使用我們所擁有的。
  5. 這個問題不僅僅是兩個結構:為了支持 3D 數學,我必須重新實現Vector1Vector2Vector3Vector4的所有內容......我不想復制/粘貼很多代碼!
  6. ...而典型的 Mesh class 有 4 個內部列表,每個列表都可以是這 4 種類型中的任何一種。 如果我必須手動編寫每個組合,而不使用 Generics,我將有4^4 = 256 個相同代碼的副本來編寫和維護。

真的讓你羡慕 C++ 和他們愚蠢的性感模板,不是嗎?

首先進行一些假設(如果錯誤,請糾正它們):

你說過網格類型可以有四個不同的列表,所以我假設它的簽名是Mesh<T1, T2, T3, T4> 我還假設您控制這種類型,但不是VectorN類型。

問題是您缺乏對 Vectors 的任何通用支持,並且您不能以任何方式對它們使用多態性。 正如您所說,將它們包裝在接口中或引入自定義類作為包裝器會降低性能。

所以你想要做的事情是雙重調度的變體 - 根據其 arguments 的類型調用不同的方法。

想到的最簡單的事情是現有FOREIGN_API調用的 static 包裝器:

public static class VectorExtensions
{
    public static int Sum<TVector>(this IEnumerable<TVector> vectors)
    {
        var type = typeof(TVector);
        if (type == typeof(Vector1))
        {
            return FOREIGN_API.Sum((IEnumerable<Vector1>)vectors);
        }
        else if (type == typeof(Vector2))
        {
            return FOREIGN_API.Sum((IEnumerable<Vector2>)vectors);
        }
        else if (...) // etc.

        throw new ArgumentException($"Invalid type of vector {typeof(TVector).Name}.");
    }
}

然后,在網格上實現Average很容易(我假設平均值是所有列表組合的平均值):

public class Mesh<T1, T2, T3, T4>
{
    private List<T1> _myVectors1;
    private List<T2> _myVectors2;
    private List<T3> _myVectors3;
    private List<T4> _myVectors4;

    public float Average()
    {
        var sum1 = _myVectors1.Sum();
        var sum2 = _myVectors2.Sum();
        var sum3 = _myVectors3.Sum();
        var sum4 = _myVectors4.Sum();

        return (float)(sum1 + sum2 + sum3 + sum4) / 
            (_myVectors1.Count + _myVectors2.Count + _myVectors3.Count + _myVectors4.Count);
    }
}

這種形式的類型檢查應該很快,因為 C# 極大地優化了typeof調用。

有一種更簡單的寫法,涉及到dynamic

public static class VectorExtensions
{
    public static int Sum<TVector>(this IEnumerable<TVector> vectors) =>
        FOREIGN_API.Sum((dynamic)vectors);
}

由於緩存, dynamic基礎架構也比許多人預期的要快,因此您可能希望先嘗試此解決方案,然后僅在診斷出性能存在問題時才考慮其他問題。 正如你所看到的,這需要非常少的代碼來嘗試。

==================================================== ============================

現在讓我們假設我們正在尋找性能最好的方法。 我非常確信沒有辦法完全避免運行時類型檢查。 在上面的案例中,每個方法調用只有少數類型檢查。 除非您調用Mesh<,,,>方法數百萬次,否則應該沒問題。 但是假設您可能想要這樣做,有一種方法可以欺騙我們擺脫這種情況。

這個想法是在您實例化網格時執行所有所需的類型檢查。 讓我們為VectorN類型中所有可能的N定義我們將調用VectorOperationsN的輔助類型。 它將實現一個接口IVectorOperations<TVector> ,它將定義您想要的基本向量操作。 現在讓我們用一個或多個向量的Sum go 來作為示例:

public interface IVectorOperations<TVector>
{
    public int Sum(TVector vector);

    public int Sum(IEnumerable<TVector> vectors);
}

public class VectorOperations1 : IVectorOperations<Vector1>
{
    public int Sum(Vector1 vector) => vector.x;

    public int Sum(IEnumerable<Vector1> vectors) => vectors.Sum(v => Sum(v));
}


public class VectorOperations2 : IVectorOperations<Vector2>
{
    public int Sum(Vector2 vector) => vector.x + vector.y;

    public int Sum(IEnumerable<Vector2> vectors) => vectors.Sum(v => Sum(v));
}

現在我們需要一種方法來獲得適當的實現——這將涉及類型檢查:

public static class VectorOperations
{
    public static IVectorOperations<TVector> GetFor<TVector>()
    {
        var type = typeof(TVector);

        if (type == typeof(Vector1))
        {
            return (IVectorOperations<TVector>)new VectorOperations1();
        }
        else if (...) // etc.

        throw new ArgumentException($"Invalid type of vector {typeof(TVector).Name}.");
    }
}

現在,當我們創建一個網格時,我們將獲得一個適當的實現,然后通過我們的方法使用它:

public class Mesh<T1, T2, T3, T4>
{
    private List<T1> _myVectors1;
    private List<T2> _myVectors2;
    private List<T3> _myVectors3;
    private List<T4> _myVectors4;
    private readonly IVectorOperations<T1> _operations1;
    private readonly IVectorOperations<T2> _operations2;
    private readonly IVectorOperations<T3> _operations3;
    private readonly IVectorOperations<T4> _operations4;

    public Mesh()
    {
        _operations1 = VectorOperations.GetFor<T1>();
        _operations2 = VectorOperations.GetFor<T2>();
        _operations3 = VectorOperations.GetFor<T3>();
        _operations4 = VectorOperations.GetFor<T4>();
    }

    public float Average()
    {
        var sum1 = _operations1.Sum(_myVectors1);
        var sum2 = _operations2.Sum(_myVectors2);
        var sum3 = _operations3.Sum(_myVectors3);
        var sum4 = _operations4.Sum(_myVectors4);

        return (float)(sum1 + sum2 + sum3 + sum4) / 
            (_myVectors1.Count + _myVectors2.Count + _myVectors3.Count + _myVectors4.Count);
    }
}

這僅在實例化網格時才有效並進行類型檢查。 成功。 但是我們可以使用兩個技巧進一步優化它。

一,我們不需要IVectorOperations<TVector>實現的新實例。 我們可以將它們設為單例,並且永遠不會為一種類型的向量實例化多個 object。 這是非常安全的,因為無論如何實現總是無狀態的。

public static class VectorOperations
{
    private static VectorOperations1 Implementation1 = new VectorOperations1();
    private static VectorOperations2 Implementation2 = new VectorOperations2();
    ... // etc.

    public static IVectorOperations<TVector> GetFor<TVector>()
    {
        var type = typeof(TVector);

        if (type == typeof(Vector1))
        {
            return (IVectorOperations<TVector>)Implementation1;
        }
        else if (...) // etc.

        throw new ArgumentException($"Invalid type of vector {typeof(TVector).Name}.");
    }
}

第二,我們不需要在每次實例化新網格時都進行類型檢查。 很容易看出,對於具有相同類型 arguments 的網格類型的每個 object,實現保持不變。 就單個封閉泛型而言,它們是 static。 因此,我們真的可以把它們做成 static:

public class Mesh<T1, T2, T3, T4>
{
    private List<T1> _myVectors1;
    private List<T2> _myVectors2;
    private List<T3> _myVectors3;
    private List<T4> _myVectors4;
    private static readonly IVectorOperations<T1> Operations1 =
        VectorOperations.GetFor<T1>();
    private static readonly IVectorOperations<T2> Operations2 =
        VectorOperations.GetFor<T2>();
    private static readonly IVectorOperations<T3> Operations3 =
        VectorOperations.GetFor<T3>();
    private static readonly IVectorOperations<T4> Operations4 =
        VectorOperations.GetFor<T4>();

    public float Average()
    {
        var sum1 = Operations1.Sum(_myVectors1);
        var sum2 = Operations2.Sum(_myVectors2);
        var sum3 = Operations3.Sum(_myVectors3);
        var sum4 = Operations4.Sum(_myVectors4);

        return (float)(sum1 + sum2 + sum3 + sum4) / 
            (_myVectors1.Count + _myVectors2.Count + _myVectors3.Count + _myVectors4.Count);
    }
}

這樣,如果有N個不同的向量類型,我們只會實例化N個實現IVectorOperations<>的對象,並執行與不同網格類型一樣多的額外類型檢查,因此最多4^N 單個網格對象不使用任何額外的 memory,但最多有4^N * 4對矢量操作實現的引用。

這仍然迫使您為不同類型實現所有向量操作四次。 但請注意,現在您已解鎖所有選項 - 您擁有一個通用界面,該界面取決於您控制的TVector類型。 允許VectorOperations實現中的任何技巧。 在通過IVectorOperations<TVector>接口與Mesh分離的同時,您可以靈活地使用。

哇這個答案很長。 感謝您來參加我的 TED 演講!

(我認為這行不通,但這是我最初嘗試 go 的方向 - 歡迎評論,也許它會激勵其他人提供更好的答案:)

我想我可以做類似的事情(如果我沒記錯的話,可能在 C++ 中,並且 C# 對於完全一般的情況沒有直接的等價物,但我認為對於像這樣的簡單情況可能有等價物):

public class Mesh<T1,T2>
{
 // This class is basically going to fail at runtime:
 //   it cannot/will not prevent you from instancing it
 //   as - say - a Mesh<string,int> - which simply cannot
 //   be sensibly implemented.
 //
 // So: many methods will throw Exceptions - but some can be implemented
 //   (and hence: shared amongst all the other variants of the class)

     public List<T1> internalList;
     public int CountElements<List<T1>>() { return internalList.Count; }
     public int DoSomethingToList1<T1>() { ... }
}

public class Mesh<Vector2,T2>
{
     // Now we're saying: HEY compiler! I'll manually override the
     //    generic instance of Mesh<T1,T2> in all cases where the
     //    T1 is a Vector2!

     public int DoSomethingToList1<Vector2>() { ... }
}

或者另一種嘗試找到一種語法上有效的方法來做同樣的事情(參見@Gserg對主要問題的評論) - 但顯然這失敗了,因為 C# 編譯器禁止任意類型轉換:

    private List<T1> data;
    public void Main()
    {
        if( typeof(T1) == typeof(Vector2) )
            Main( (List<Vector2>) data );
        else if( typeof(T1) == typeof(Vector3) )
            Main( (List<Vector3>) data );
    }

    public void Main( List<Vector2> dataVector2s )
    {
        ...
    }
    public void Main( List<Vector3> dataVector3s )
    {
        ...
    }

我不確定這是否是你想要的,但也許你可以通過一點運行時編譯來解決這個問題。 例如,您可以生成一個對結構的字段求和的委托;

        public Func<T, int> Sum { get; private set; }
        public void Compile()
        {
            var parm = Expression.Parameter(typeof(T), "parm");
            Expression sum = null;

            foreach(var p in typeof(T).GetFields())
            {
                var member = Expression.MakeMemberAccess(parm, p);
                sum = sum == null ? (Expression)member : Expression.Add(sum, member);
            }
            Sum = Expression.Lambda<Func<T, int>>(sum, parm).Compile();
        }

或者也許只是一種將結構轉換為其他類型的更易於使用的可枚舉的方法。

我的建議是讓 Vector2 和 Vector3 自帶處理方法。 接口是您正在尋找的機器人:

  • 讓他們實現具有您需要的功能的接口
  • 使用該接口作為提交到您列表中的任何內容的類型
  • 調用接口函數

顯示過程的合適名稱是“Sumable”。

這些可能是Vector struct的內置實現。 當然這兩個是不能繼承的。 但是MVVM的做事方式是:“如果你不能繼承或修改它,就將它包裝成你可以繼承和修改的東西”。

一個簡單的包裝器(它可以是結構或類)圍繞實現接口的那些向量之一就是您所需要的。

另一種選擇是使用 LINQ 進行處理。 如果它只是一個一次性的過程,那么它通常比繼承、類、接口等要輕得多。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM