【问题标题】:generic function with a "has property X" constraint?具有“具有属性 X”约束的通用函数?
【发布时间】:2009-08-20 17:23:44
【问题描述】:

我有一个第三方闭源应用程序,它导出一个 COM 接口,我通过 Interop 在我的 C#.NET 应用程序中使用该接口。这个 COM 接口导出许多对象,这些对象都显示为 System.Object,直到我将它们转换为适当的接口类型。我想分配所有这些对象的属性。因此:

foreach (object x in BigComInterface.Chickens)
{
    (x as Chicken).attribute = value;
}
foreach (object x in BigComInterface.Ducks)
{
    (x as Duck).attribute = value;
}

但是分配属性很可能(出于不可避免的特定于应用程序的原因)抛出我想从中恢复的异常,所以我真的想要对每个异常进行尝试/捕获。因此:

foreach (object x in BigComInterface.Chickens)
{
    try
    {
        (x as Chicken).attribute = value;
    }
    catch (Exception ex)
    {
        // handle...
    }
}
foreach (object x in BigComInterface.Ducks)
{
    try
    {
        (x as Duck).attribute = value;
    }
    catch (Exception ex)
    {
        // handle...
    }
}

显然,这样做会干净得多:

foreach (object x in BigComInterface.Chickens)
{
    SetAttribute<Chicken>(x as Chicken, value);
}
foreach (object x in BigComInterface.Ducks)
{
    SetAttribute<Duck>(x as Duck, value);
}

void SetAttribute<T>(T x, System.Object value)
{
    try
    {
        x.attribute = value;
    }
    catch
    {
        // handle...
    }
}

看到问题了吗?我的 x 值可以是任何类型,因此编译器无法解析 .attribute。 Chicken 和 Duck 不在任何类型的继承树中,它们不共享具有 .attribute 的接口。如果他们这样做了,我可以在 T 上对该接口进行约束。但由于该课程是闭源的,这对我来说是不可能的。

在我的幻想中,我想要的是类似于一个约束,要求参数具有 .attribute 属性,无论它是否实现给定的接口。也就是说,

void SetAttribute<T>(T x, System.Object value) where T:hasproperty(attribute)

除了为鸡、鸭、牛、羊等每个人剪切/粘贴这个小尝试/捕获块之外,我不知道从这里做什么。

我的问题是:当在编译时无法知道实现该属性的接口时,想要调用对象上的特定属性的问题有什么好的解决方法?

【问题讨论】:

    标签: c# generics properties interface


    【解决方案1】:

    嗯,根据您的异常处理代码的庞大程度(如果我没记错的话,可能是这样),使用以下技巧可能会对您有所帮助:

    class Chicken
    {
        public string attribute { get; set; }
    }
    
    class Duck
    {
        public string attribute { get; set; }
    }
    
    interface IHasAttribute
    {
        string attribute { get; set; }
    }
    
    class ChickenWrapper : IHasAttribute
    {
        private Chicken chick = null;
        public string attribute
        {
            get { return chick.attribute; }
            set { chick.attribute = value; }
        }
        public ChickenWrapper(object chick)
        {
            this.chick = chick as Chicken;
        }
    }
    
    class DuckWrapper : IHasAttribute
    {
        private Duck duck = null;
        public string attribute
        {
            get { return duck.attribute; }
            set { duck.attribute = value; }
        }
        public DuckWrapper(object duck)
        {
            this.duck = duck as Duck;
        }
    }
    
    void SetAttribute<T>(T x, string value) where T : IHasAttribute
    {
        try
        {
            x.attribute = value;
        }
        catch
        {
            // handle...
        }
    }
    

    【讨论】:

    • 这最终是最好的解决方案。包装类允许您获取第三方代码并应用您自己的继承关系等。它还将使您免受对第三方 API 的更改。
    • 这通常是一个很好的解决方案,但我最初的问题是第三方闭源应用程序正在通过 Interop 将 COM 中的鸡和鸭交给我,包装在 System.Objects 中。我可以按照您的建议将它们包装在 ChickenWrappers 和 DuckWrappers 中,但要做到这一点,我必须遍历 所有 可用的对象,尝试投射到 Duck、Chicken、Swan、Goose 等,然后将结果包装在 DuckWrapper、ChickenWrapper、SwanWrapper 或 GooseWrapper 等中。那将是一个可怕的 switch 语句。
    • 实际上,看一下 Wrapper 构造函数:它接受一个对象并在那里进行转换,所以你只会做“new ChickenWrapper(o)”而不是“(Chicken)o”
    【解决方案2】:

    不幸的是,目前这很棘手。在 C# 4 中,动态类型可能对此有很大帮助。 COM 互操作是 dynamic 真正大放异彩的地方之一。

    但是,与此同时,允许您拥有任何类型的对象且对接口没有限制的唯一选择是恢复使用反射。

    您可以使用反射找到“属性”属性,并在运行时设置它的值。

    【讨论】:

    • 谢谢你,里德。我确实尝试了反射技术——它可以满足我的需要,但有点慢。
    • 由于没有通用接口,因此无法进行基于 vtable 的快速泛型调用 - 您必须对名称进行动态查找,这总是很慢,无论是否反射。
    • @Pavel:是的,它总是很慢,但从技术上讲,在 C# 4 出来之前,它是在不更改现有类的情况下工作的唯一方法。我并不是说它很好,而是说它会起作用。
    【解决方案3】:

    不幸的是,这样做的唯一方法是使用定义该属性并在所有类型上实现的接口来约束类型参数。

    由于您没有源,这将无法实现,因此您将不得不使用某种解决方法。 C# 是静态类型的,因此不支持您要在此处使用的那种鸭子类型。即将到来的最好的事情(在 C# 4 中)是将对象键入为 dynamic 并在执行时解析属性调用(请注意,这种方法也不是通用的,因为您不能将泛型类型参数约束为 dynamic )。

    【讨论】:

    • 你可以通过反射来做到这一点。
    • 使用动态,您不需要泛型参数。只需使用将值作为动态的单一方法,然后尝试设置 obj.attribute。它应该可以正常工作,前提是“属性”作为 obj 上的属性存在。
    • 对 - 我也是这么想的,但我想明确一点,所有静态类型检查都将消失 :)
    【解决方案4】:

    为了不必违反 DRY 原则,您可以使用反射。如果您真的想使用泛型,您可以为每种类型的 3rd 方对象使用一个包装器类,并让包装器实现一个接口,您可以使用该接口来约束泛型类型参数。

    一个如何使用反射完成的示例。但是代码没有经过测试。

        void SetAttribute(object chickenDuckOrWhatever, string attributeName, string value)
        {
            var typeOfObject = chickenDuckOrWhatever.GetType();
            var property = typeOfObject.GetProperty(attributeName);
            if (property != null)
            {
                property.SetValue(chickenDuckOrWhatever, value);
                return;
            }
    
            //No property with this name was found, fall back to field
            var field = typeOfObject.GetField(attributeName);
            if (field == null) throw new Exception("No property or field was found on type '" + typeOfObject.FullName + "' by the name of '" + attributeName + "'.");
            field.SetValue(chickenDuckOrWhatever, value);
        }
    

    如果您为了提高性能而加快代码速度,您可以缓存 chickenDuckOrWhatever 的每个类型的 FieldInfo 和 PropertyInfo,并在反映之前先查阅字典。

    提示:不必将attributeName 硬编码为字符串,您可以使用C# 6 功能nameof。比如nameof(Chicken.AttributeName)。

    【讨论】:

      猜你喜欢
      • 2022-01-09
      • 2023-04-07
      • 2021-01-30
      • 1970-01-01
      • 1970-01-01
      • 2023-03-24
      • 1970-01-01
      • 1970-01-01
      • 2021-03-08
      相关资源
      最近更新 更多