Общая функция с ограничением "имеет свойство X"?

У меня есть стороннее закрытое исходное приложение, которое экспортирует COM-интерфейс, который я использую в своем приложении С#.NET через Interop. Этот 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 может быть любого типа, поэтому компилятор не может разрешить.трибут. Цыпленок и утка не имеют никакого дерева наследования и не имеют интерфейса с атрибутом. Если бы они это сделали, я мог бы установить ограничение для этого интерфейса на T. Но поскольку класс является закрытым, это невозможно для меня.

То, что я хочу, в моей фантазии, является чем-то вроде ограничения, требующего, чтобы аргумент имел свойство .attribute независимо от того, реализует ли он данный интерфейс. Для этого

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

Я не уверен, что делать дальше, кроме как вырезать/вставить этот маленький блок try/catch для каждого из Chicken, Duck, Cow, Sheep и т.д.

Мой вопрос: что является хорошим обходным путем для этой проблемы, требующей вызова определенного свойства для объекта, когда интерфейс, реализующий это свойство, не может быть известен во время компиляции?

Ответы

Ответ 1

К сожалению, сейчас это сложно. В С# 4 динамический тип может немного помочь с этим. COM-взаимодействие - это одно из мест, в котором действительно сияет динамика.

Тем не менее, в то же время единственный вариант, который позволяет вам иметь любой тип объекта без ограничений на интерфейсы, - это вернуться к использованию отражения.

Вы можете использовать отражение, чтобы найти свойство "attribute", и установить его значение во время выполнения.

Ответ 2

Хорошо, в зависимости от того, насколько ваш код обработки исключений (и если я не ошибаюсь, это может быть так), используя следующий трюк, может помочь вам:

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

Ответ 3

К сожалению, единственный способ сделать это - ограничить параметр типа интерфейсом, который определяет это свойство и реализуется на всех типах.

Поскольку у вас нет источника, это будет невозможно реализовать, и поэтому вам придется использовать какое-то обходное решение. С# статически типизирован и, как таковой, не поддерживает тип утиного ввода, который вы хотите использовать здесь. Лучшее в ближайшее время (в С# 4) - это ввести объект как dynamic и разрешить вызовы свойств во время выполнения (обратите внимание, что этот подход также не был бы общим, поскольку вы не можете ограничить параметр типового типа как dynamic).

Ответ 4

Чтобы не нарушать принцип DRY, вы можете использовать отражение. Если вы действительно хотите использовать generics, вы можете использовать класс-оболочку для каждого типа стороннего объекта и иметь оболочку, реализующую интерфейс, который вы можете использовать для ограничения аргумента общего типа.

Пример того, как это можно сделать с отражением. Код не проверен.

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

Если вы ускорите выполнение кода для производительности, вы можете кэшировать FieldInfo и PropertyInfo для каждого типа chickenDuckOrWhatever и сначала ознакомиться со словарем.

СОВЕТ. Чтобы не было жесткого кода attributeName в виде строки, вы могли бы использовать функцию С# 6 nameof. Например, имяof (Chicken.AttributeName).