【问题标题】:C# generic collectionsC# 泛型集合
【发布时间】:2011-01-03 03:55:47
【问题描述】:

我是从 Java 背景开始使用 C# 的,并且不断遇到与泛型相同的问题,而这些问题在 Java 中很容易解决。

给定类:

interface IUntypedField { }
class Field<TValue> : IUntypedField { }

interface IFieldMap
{
    void Put<TValue>(Field<TValue> field, TValue value);
    TValue Get<TValue>(Field<TValue> field);
}

我想写一些类似的东西:

class MapCopier
{
    void Copy(IEnumerable<IUntypedField> fields, IFieldMap from, IFieldMap to)
    {
        foreach (var field in fields)
            Copy(field, from, to); // <-- clearly doesn't compile as field is IUntypedField not Field 
    }

    void Copy<TValue>(Field<TValue> field, IFieldMap from, IFieldMap to)
    {
        to.Put(field, from.Get(field));
    }
}

在 Java 中,这很容易解决,因为字段集合是 Iterable&lt;Field&lt;?&gt;&gt;,您可以直接调用 Copy(Field, IFieldMap, IFieldMap)

在 C# 中,我发现自己对 TValue 的所有可能值进行了切换/转换(这闻起来很可怕,必须为您添加的每种类型添加一个案例显然是一个等待发生的错误,并且只有当集合类型是有限的):

foreach (var field in fields)
{
    switch (field.Type) // added an enum to track the type of Field's parameterised type
    {
    case Type.Int:   Copy((Field<int>)field, from, to); break;
    case Type.Long:  Copy((Field<long>)field, from, to); break; 
    ...
    }
}

我有时会做的另一个选择是将功能移到 Field 类中,这又很臭。这不是该领域的责任。至少这避免了巨大的开关:

interface IUntypedField { void Copy(IFieldMap from, IFieldMap to); }
class Field<TValue> : IUntypedField 
{ 
    void Copy(IFieldMap from, IFieldMap to)
    {
        to.Put(this, from.Get(this));
    }
}

...

    foreach (var field in fields)
        field.Copy(from, to);

如果您可以编写多态扩展方法(即上面 IUntypedField 和 Field 中的 Copy 方法),那么您至少可以将代码保留在其职责所在的类中。

我是否遗漏了 C# 的某些功能,使这成为可能。还是有一些可以使用的功能模式?有什么想法吗?

(最后一件事,我目前被困在 .Net 3.5 上,因此无法使用任何协变/逆变,但仍然有兴趣知道它们将如何提供帮助(如果有的话)。

【问题讨论】:

  • 我不确定我是否理解你的模式,你能提供一个 IFieldMap 的实现吗?
  • @Yads,实现在这里并不重要。正如 siride 所指出的那样,它将在内部将事物存储为对象并且必须进行转换,但就接口的消费者而言,它是放置和获取真实类型。
  • @Claus,该问题的答案假设用户想要使用同质列表(如 Danny Chen 的答案),这是一个不同的问题。
  • 重点更多的是你发现在 Java 中更容易的原因是因为你使用了通配符。因此,他应该学习 Java 通配符的 C#“替代品”。

标签: c# generics


【解决方案1】:

您至少可以使用反射来避免switch { }。在这种情况下,来自 fw 4.0 的协变/逆变将无济于事。也许使用dynamic 关键字可能会受益。

// untested, just demonstates concept
class MapCopier
{
    private static void GenericCopy<TValue>(Field<TValue> field, IFieldMap from, IFieldMap to)
    {
        to.Put(field, from.Get(field));
    }

    public void Copy(IEnumerable<IUntypedField> fields, IFieldMap from, IFieldMap to)
    {
        var genericMethod = typeof(MapCopier).GetMethod("GenericCopy");
        foreach(var field in fields)
        {
            var type = field.GetType().GetGenericArguments()[0];
            var method = genericMethod.MakeGenericMethod(type);
            method.Invoke(null, new object[] { field, from, to });
        }
    }
}

【讨论】:

  • 这是迄今为止我见过的最差的解决方案(我仍然看到用于调用方法的反射)。我需要对此进行一些性能测试,看看它与将功能留在 Field 类中相比有多糟糕。
  • 如果您缓存方法(每种类型 1 个),您可能可以在微秒内完成反射。如果您每秒执行数千份副本,这应该没问题,但如果您每秒执行数百万份,则太慢了。
  • 我已经用 Type => MethodInfo 的缓存做了一个快速的基准测试(不是locked,因为我不需要它)。 10,000 次基于反射的 Copy 调用(包含 10 个字段)需要约 550 毫秒,而调用 Field.Copy 的映射 Copy 需要约 30 毫秒。在我的情况下,这可能没有足够大的差异引起注意,但它远非理想。
  • SimonC:您还可以在运行时使用Expression 树编译方法,这将以“本机”速度运行。如果您有兴趣,我可以发布一个示例。
  • @Gabe,我刚刚尝试了Expression 树方法,并被性能所震撼。地图复制平均花费了约 40 毫秒(第一个是约 80 毫秒来编译所有表达式树)。如果您想发布答案,我会将其标记为已接受。
【解决方案2】:

这是一个完美的类型安全方法,它编译 lambda 以执行复制:

static class MapCopier
{
    public static void Copy(IEnumerable<IUntypedField> fields, IFieldMap from, IFieldMap to)
    {
        foreach (var field in fields)
            Copy(field, from, to);
    }

    // cache generated Copy lambdas
    static Dictionary<Type, Action<IUntypedField, IFieldMap, IFieldMap>> copiers =
        new Dictionary<Type, Action<IUntypedField, IFieldMap, IFieldMap>>();

    // generate Copy lambda based on passed-in type
    static void Copy(IUntypedField field, IFieldMap from, IFieldMap to)
    {
        // figure out what type we need to look up;
        // we know we have a Field<TValue>, so find TValue
        Type type = field.GetType().GetGenericArguments()[0];
        Action<IUntypedField, IFieldMap, IFieldMap> copier;
        if (!copiers.TryGetValue(type, out copier))
        {
            // copier not found; create a lambda and compile it
            Type tFieldMap = typeof(IFieldMap);
            // create parameters to lambda
            ParameterExpression
                fieldParam = Expression.Parameter(typeof(IUntypedField)),
                fromParam = Expression.Parameter(tFieldMap),
                toParam = Expression.Parameter(tFieldMap);
            // create expression for "(Field<TValue>)field"
            var converter = Expression.Convert(fieldParam, field.GetType());
            // create expression for "to.Put(field, from.Get(field))"
            var copierExp =
                Expression.Call(
                    toParam,
                    tFieldMap.GetMethod("Put").MakeGenericMethod(type),
                    converter,
                    Expression.Call(
                        fromParam,
                        tFieldMap.GetMethod("Get").MakeGenericMethod(type),
                        converter));
            // create our lambda and compile it
            copier =
                Expression.Lambda<Action<IUntypedField, IFieldMap, IFieldMap>>(
                    copierExp,
                    fieldParam,
                    fromParam,
                    toParam)
                    .Compile();
            // add the compiled lambda to the cache
            copiers[type] = copier;
        }
        // invoke the actual copy lambda
        copier(field, from, to);
    }

    public static void Copy<TValue>(Field<TValue> field, IFieldMap from, IFieldMap to)
    {
        to.Put(field, from.Get(field));
    }
}

请注意,此方法即时创建复制方法,而不是调用Copy&lt;TValue&gt; 方法。这本质上是内联的,并且由于没有额外的调用,每次调用可以节省大约 50ns。如果您要使Copy 方法更复杂,调用Copy 可能比创建表达式树来内联它更容易。

【讨论】:

    【解决方案3】:

    既然 Java 只能通过类型擦除来解决这个问题,为什么不用非泛型版本来模拟擦除呢?这是一个例子:

    interface IUntypedField { }
    class Field<TValue> : IUntypedField { }
    
    interface IFieldMap
    {
        void Put<TValue>(Field<TValue> field, TValue value);
        TValue Get<TValue>(Field<TValue> field);
        void PutObject(IUntypedField field, object value); // <-- this has a cast
                                                     // to TValue for type safety
        object GetObject(IUntypedField field);
    }
    
    class MapCopier
    {
        void Copy(IEnumerable<IUntypedField> fields, IFieldMap from, IFieldMap to)
        {
            foreach (var field in fields)
                Copy(field, from, to); // <-- now compiles because
                                       // it calls non-generic overload
        }
    
        // non-generic overload of Copy
        void Copy(IUntypedField field, IFieldMap from, IFieldMap to)
        {
            to.PutObject(field, from.GetObject(field));
        }
    
        void Copy<TValue>(Field<TValue> field, IFieldMap from, IFieldMap to)
        {
            to.Put(field, from.Get(field));
        }
    }
    

    【讨论】:

    • 不幸的是,类型擦除模拟方法没有像 Java 那样进行编译时检查。
    【解决方案4】:

    您有一个问题:您要求编译器做出只能在运行时做出的决定。可枚举集中的每个实例都应该为Copy() 提供泛型参数的类型。但是这个决定必须在编译时做出。这是一个矛盾。由于类型擦除,Java 可以摆脱它。由于您想要相同的行为,因此最好的办法是使第二个 Copy() 方法成为非泛型方法,而只将 IUnknownField 或任何适合的基接口或类作为其第一个参数。

    详细说明这一点,我不得不问你让IFieldMapinterface 有一个通用的Put() 方法获得了什么。在内部,它必须将项目存储在某种异构集合中(这意味着您必须在某个地方拥有objects 的列表或字典)。所以泛型实际上并不提供任何类型安全。您不妨摆脱它们并拥有Put()Get() 方法,以及MapCopier 类中的Copy() 采用object 或一些合适的基本接口,例如IUnknownField。因为当你真正开始工作时,这就是你真正的工作。

    【讨论】:

    • 不管它是如何在内部存储的,编译器都会为接口的用户提供类型安全。我宁愿在映射中发生类转换异常,也不愿在使用值的位置远离它(因此不是错误所在的位置)。
    • @SimonC 但编译器并没有 为您提供任何类型安全性。您对泛型参数没有任何限制。此外,没有办法对异构集合进行编译时类型检查。尽管它确实是一个实现细节,但您并不要求,甚至不建议 FieldMap 中的条目是同质的。由于它们不是,因此您必须依靠运行时检查您自己的代码。编译器不能为您做任何事情,至少对于Put() 的情况不能。在Get() 的情况下,它会同时失败并且...
    • ...原因与非通用案例相同。
    • 也许我应该切入正题:接口有这些方法很好。但它也应该有可供复印机和其他批量操作使用的非通用版本。
    • 接口的使用者会有一些信心,如果他们有一个字段f = new Field&lt;int&gt;(),编译器将强制他们不能做map.Put(f, "foo")bool bar = map.Get(f)。如果实现有任何问题,将在实现中抛出异常(而不是在用户代码中使用来自map.Get 的值的点)。如果我允许基于非泛型 object 的方法,消费者对地图中的值类型的信心就会降低。
    【解决方案5】:

    如果您不想使用反射,那么您可能不想使用泛型。这是一个允许这样做的代码:

    interface IUntypedField { }
    
    abstract class Field
    {
        protected Field(Type type)
        {
            FieldType = type;
        }
    
        public Type FieldType { get; private set; }
    }
    
    class Field<TValue> : Field, IUntypedField {
    
        public Field()
            : base(typeof(TValue))
        {
        }
    }
    
    interface IFieldMap
    {
        void Put(Field field, object value);
        object Get(Field field);
    }
    
    class MapCopier
    {
        void Copy(IEnumerable<IUntypedField> fields, IFieldMap from, IFieldMap to)
        {
            foreach (var field in fields)
            {
                Field f = field as Field;
                if (f != null)
                {
                    Copy(f, from, to);
                }
            }
        }
    
        void Copy(Field field, IFieldMap from, IFieldMap to)
        {
            to.Put(field, from.Get(field));
        }
    }
    

    在这段代码中,每个 Field 都派生自一个动态携带字段类型的类(不使用泛型),但您仍然拥有泛型类。您将在 .NET Framework 本身中看到这种模式,尤其是在 System.ServiceModel 类中(围绕 Channel/ChannelFactory 类型)。

    缺点是在这种情况下您必须对 IFieldMap 接口进行反生成。但是,当您实现它时,您可能会发现那里也无法使用泛型。

    另一种可能性是直接在 IUntypedField 上添加 FieldType 属性,避免使用抽象类 Field。

    【讨论】:

    • 我实际上正在采用一种混合方法,正如您所建议的,我将类型存储在 IUntypedField 上的字段中,并作为字段上的类型参数。在可能的情况下,我使用泛型接口,因为有更好的编译时间检查,但是在我 必须 的地方,我将退回到使用非泛型接口进行运行时类型检查。我真的希望我错过了一些语言特性或巧妙地使用函数式编程来做到这一点,但可惜我没有。
    猜你喜欢
    • 2015-05-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-03
    相关资源
    最近更新 更多