Общая функция с ограничением "имеет свойство 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).