Интерфейс, определяющий сигнатуру конструктора?
Странно, что я впервые столкнулся с этой проблемой, но:
Как вы определяете конструктор в интерфейсе C#?
редактировать
Некоторые люди хотели пример (это проект свободного времени, так что да, это игра)
IDrawable
+ Обновление
+Draw
Чтобы иметь возможность обновлять (проверять края экрана и т. Д.) И рисовать себя, для этого всегда потребуется GraphicsDeviceManager
, Поэтому я хочу убедиться, что объект имеет ссылку на него. Это будет принадлежать в конструкторе.
Теперь, когда я записал это, я думаю, что я реализую здесь IObservable
и GraphicsDeviceManager
должен взять IDrawable
... Кажется, либо я не понимаю структуру XNA, либо структура не очень хорошо продумана.
редактировать
Кажется, есть некоторая путаница в моем определении конструктора в контексте интерфейса. Интерфейс действительно не может быть создан, поэтому ему не нужен конструктор. То, что я хотел определить, было подписью для конструктора. Точно так же, как интерфейс может определять сигнатуру определенного метода, интерфейс может определять сигнатуру конструктора.
17 ответов
Как уже отмечалось, у вас не может быть конструкторов интерфейса. Но так как этот результат получил столь высокий рейтинг в Google около 7 лет спустя, я подумал, что я хотел бы добавить сюда - в частности, чтобы показать, как вы можете использовать абстрактный базовый класс в тандеме с вашим существующим интерфейсом и, возможно, сократить объем рефакторинга. необходимо в будущем для подобных ситуаций. Эта концепция уже упоминалась в некоторых комментариях, но я подумал, что стоило бы показать, как на самом деле это сделать.
Итак, у вас есть основной интерфейс, который выглядит так:
public interface IDrawable
{
void Update();
void Draw();
}
Теперь создайте абстрактный класс с конструктором, который вы хотите применить. На самом деле, поскольку он теперь доступен с момента написания исходного вопроса, мы можем немного придумать и использовать обобщенные значения в этой ситуации, чтобы адаптировать его к другим интерфейсам, которые могут нуждаться в той же функциональности, но с другими требованиями к конструктору:
public abstract class MustInitialize<T>
{
public MustInitialize(T parameters)
{
}
}
Теперь вам нужно создать новый класс, который наследует как от интерфейса IDrawable, так и от абстрактного класса MustInitialize:
public class Drawable : MustInitialize<GraphicsDeviceManager>, IDrawable
{
GraphicsDeviceManager _graphicsDeviceManager;
public Drawable(GraphicsDeviceManager graphicsDeviceManager)
: base (graphicsDeviceManager)
{
_graphicsDeviceManager = graphicsDeviceManager;
}
public void Update()
{
//use _graphicsDeviceManager here to do whatever
}
public void Draw()
{
//use _graphicsDeviceManager here to do whatever
}
}
Затем просто создайте экземпляр Drawable, и все готово:
IDrawable drawableService = new Drawable(myGraphicsDeviceManager);
Крутая вещь в том, что новый класс Drawable, который мы создали, по-прежнему ведет себя так же, как мы ожидаем от IDrawable.
Если вам нужно передать более одного параметра в конструктор MustInitialize, вы можете создать класс, который определяет свойства для всех полей, которые вам нужно передать.
Ты не можешь Иногда это боль, но вы все равно не сможете назвать это обычными методами.
В своем блоге я предложил статические интерфейсы, которые можно было бы использовать только в ограничениях общего типа, но которые могли бы быть очень полезными, IMO.
Одно замечание: если бы вы могли определить конструктор в интерфейсе, у вас возникли бы проблемы с производством классов:
public class Foo : IParameterlessConstructor
{
public Foo() // As per the interface
{
}
}
public class Bar : Foo
{
// Yikes! We now don't have a parameterless constructor...
public Bar(int x)
{
}
}
Очень поздний вклад демонстрирует еще одну проблему с сопряженными конструкторами. (Я выбираю этот вопрос, потому что в нем четко сформулирована проблема). Предположим, мы могли бы иметь:
interface IPerson
{
IPerson(string name);
}
interface ICustomer
{
ICustomer(DateTime registrationDate);
}
class Person : IPerson, ICustomer
{
Person(string name) { }
Person(DateTime registrationDate) { }
}
Где по соглашению реализация "конструктора интерфейса" заменяется именем типа.
Теперь сделайте экземпляр:
ICustomer a = new Person("Ernie");
Будем ли мы сказать, что контракт ICustomer
подчиняется?
И что по этому поводу:
interface ICustomer
{
ICustomer(string address);
}
Ты не можешь
Интерфейсы определяют контракты, которые реализуют другие объекты, и поэтому не имеют состояния, которое необходимо инициализировать.
Если у вас есть состояние, которое нужно инициализировать, вам следует рассмотреть возможность использования абстрактного базового класса.
Я оглядывался назад на этот вопрос и подумал про себя: может быть, мы неправильно подходим к этой проблеме. Интерфейсы могут не подходить для определения конструктора с определенными параметрами... но (абстрактный) базовый класс есть.
Если вы создаете базовый класс с конструктором, который принимает необходимые параметры, каждый класс, который его получает, должен предоставить их.
public abstract class Foo
{
protected Foo(SomeParameter x)
{
this.X = x;
}
public SomeParameter X { get; private set }
}
public class Bar : Foo // Bar inherits from Foo
{
public Bar()
: base(new SomeParameter("etc...")) // Bar will need to supply the constructor param
{
}
}
Невозможно создать интерфейс, который определяет конструкторы, но возможно определить интерфейс, который вынуждает тип иметь конструктор без параметров, хотя это будет очень уродливый синтаксис, который использует дженерики... На самом деле я не уверен, что это действительно хороший шаблон кодирования.
public interface IFoo<T> where T : new()
{
void SomeMethod();
}
public class Foo : IFoo<Foo>
{
// This will not compile
public Foo(int x)
{
}
#region ITest<Test> Members
public void SomeMethod()
{
throw new NotImplementedException();
}
#endregion
}
С другой стороны, если вы хотите проверить, имеет ли тип конструктор без параметров, вы можете сделать это с помощью отражения:
public static class TypeHelper
{
public static bool HasParameterlessConstructor(Object o)
{
return HasParameterlessConstructor(o.GetType());
}
public static bool HasParameterlessConstructor(Type t)
{
// Usage: HasParameterlessConstructor(typeof(SomeType))
return t.GetConstructor(new Type[0]) != null;
}
}
Надеюсь это поможет.
Одним из способов решения этой проблемы является использование обобщений и ограничения new().
Вместо того, чтобы выражать ваш конструктор как метод / функцию, вы можете выразить его как фабричный класс / интерфейс. Если вы укажете общее ограничение new() на каждом сайте вызовов, для которого необходимо создать объект вашего класса, вы сможете соответствующим образом передавать аргументы конструктора.
Для вашего примера IDrawable:
public interface IDrawable
{
void Update();
void Draw();
}
public interface IDrawableConstructor<T> where T : IDrawable
{
T Construct(GraphicsDeviceManager manager);
}
public class Triangle : IDrawable
{
public GraphicsDeviceManager Manager { get; set; }
public void Draw() { ... }
public void Update() { ... }
public Triangle(GraphicsDeviceManager manager)
{
Manager = manager;
}
}
public TriangleConstructor : IDrawableConstructor<Triangle>
{
public Triangle Construct(GraphicsDeviceManager manager)
{
return new Triangle(manager);
}
}
Теперь, когда вы используете это:
public void SomeMethod<TBuilder>(GraphicsDeviceManager manager)
where TBuilder: IDrawableConstructor<Triangle>, new()
{
// If we need to create a triangle
Triangle triangle = new TBuilder().Construct(manager);
// Do whatever with triangle
}
Вы даже можете сконцентрировать все методы создания в одном классе, используя явную реализацию интерфейса:
public DrawableConstructor : IDrawableConstructor<Triangle>,
IDrawableConstructor<Square>,
IDrawableConstructor<Circle>
{
Triangle IDrawableConstructor<Triangle>.Construct(GraphicsDeviceManager manager)
{
return new Triangle(manager);
}
Square IDrawableConstructor<Square>.Construct(GraphicsDeviceManager manager)
{
return new Square(manager);
}
Circle IDrawableConstructor<Circle>.Construct(GraphicsDeviceManager manager)
{
return new Circle(manager);
}
}
Чтобы использовать это:
public void SomeMethod<TBuilder, TShape>(GraphicsDeviceManager manager)
where TBuilder: IDrawableConstructor<TShape>, new()
{
// If we need to create an arbitrary shape
TShape shape = new TBuilder().Construct(manager);
// Do whatever with the shape
}
Другой способ - использовать лямбда-выражения в качестве инициализаторов. В какой-то момент в начале иерархии вызовов вы будете знать, какие объекты вам нужно создать (например, когда вы создаете или получаете ссылку на ваш объект GraphicsDeviceManager). Как только у вас это получится, пройдите лямбду
() => new Triangle(manager)
к последующим методам, чтобы они знали, как с этого момента создать треугольник. Если вы не можете определить все возможные методы, которые вам понадобятся, вы всегда можете создать словарь типов, которые реализуют IDrawable, используя отражение, и зарегистрировать лямбда-выражение, показанное выше, в словаре, который можно либо сохранить в общем расположении, либо передать в дальнейшие вызовы функций.
Один из способов решить эту проблему, которую я нашел, - разделить конструкцию на отдельный завод. Например, у меня есть абстрактный класс с именем IQueueItem, и мне нужен способ перевода этого объекта в другой объект и из него (CloudQueueMessage). Так что на интерфейсе IQueueItem у меня есть -
public interface IQueueItem
{
CloudQueueMessage ToMessage();
}
Теперь мне также нужен способ для моего фактического класса очереди преобразовать CloudQueueMessage обратно в IQueueItem - то есть необходимость статической конструкции, такой как IQueueItem objMessage = ItemType.FromMessage. Вместо этого я определил другой интерфейс IQueueFactory -
public interface IQueueItemFactory<T> where T : IQueueItem
{
T FromMessage(CloudQueueMessage objMessage);
}
Теперь я могу наконец написать свой общий класс очереди без ограничения new(), которое в моем случае было главной проблемой.
public class AzureQueue<T> where T : IQueueItem
{
private IQueueItemFactory<T> _objFactory;
public AzureQueue(IQueueItemFactory<T> objItemFactory)
{
_objFactory = objItemFactory;
}
public T GetNextItem(TimeSpan tsLease)
{
CloudQueueMessage objQueueMessage = _objQueue.GetMessage(tsLease);
T objItem = _objFactory.FromMessage(objQueueMessage);
return objItem;
}
}
Теперь я могу создать экземпляр, который удовлетворяет критериям для меня
AzureQueue<Job> objJobQueue = new JobQueue(new JobItemFactory())
надеюсь, это когда-нибудь поможет кому-то еще, очевидно, много внутреннего кода удалено, чтобы попытаться показать проблему и решение
Общий фабричный подход все еще кажется идеальным. Вы бы знали, что фабрике требуется параметр, и так получилось, что эти параметры передаются конструктору объекта, который создается.
Обратите внимание, что это просто проверенный синтаксис псевдокод, возможно, здесь есть предупреждение во время выполнения:
public interface IDrawableFactory
{
TDrawable GetDrawingObject<TDrawable>(GraphicsDeviceManager graphicsDeviceManager)
where TDrawable: class, IDrawable, new();
}
public class DrawableFactory : IDrawableFactory
{
public TDrawable GetDrawingObject<TDrawable>(GraphicsDeviceManager graphicsDeviceManager)
where TDrawable : class, IDrawable, new()
{
return (TDrawable) Activator
.CreateInstance(typeof(TDrawable),
graphicsDeviceManager);
}
}
public class Draw : IDrawable
{
//stub
}
public class Update : IDrawable {
private readonly GraphicsDeviceManager _graphicsDeviceManager;
public Update() { throw new NotImplementedException(); }
public Update(GraphicsDeviceManager graphicsDeviceManager)
{
_graphicsDeviceManager = graphicsDeviceManager;
}
}
public interface IDrawable
{
//stub
}
public class GraphicsDeviceManager
{
//stub
}
Пример возможного использования:
public void DoSomething()
{
var myUpdateObject = GetDrawingObject<Update>(new GraphicsDeviceManager());
var myDrawObject = GetDrawingObject<Draw>(null);
}
Конечно, вам нужно, чтобы экземпляры create только через фабрику гарантировали, что у вас всегда будет правильно инициализированный объект. Возможно, использование инфраструктуры внедрения зависимостей, такой как AutoFac, имело бы смысл; Update() может "запросить" контейнер IoC для нового объекта GraphicsDeviceManager.
Вы можете сделать это с помощью трюка с дженериками, но он все еще уязвим к тому, что написал Джон Скит
public interface IHasDefaultConstructor<T> where T : IHasDefaultConstructor<T>, new()
{
}
Класс, реализующий этот интерфейс, должен иметь конструктор без параметров:
public class A : IHasDefaultConstructor<A> //Notice A as generic parameter
{
public A(int a) { } //compile time error
}
Целью интерфейса является принудительное использование определенной подписи объекта. Это явно не должно касаться того, как объект работает внутри. Следовательно, конструктор в интерфейсе не имеет смысла с концептуальной точки зрения.
Есть несколько альтернатив:
Создайте абстрактный класс, который действует как минимальная реализация по умолчанию. В этом классе должны быть конструкторы, которые, как вы ожидаете, будут иметь реализующие классы.
Если вы не возражаете против перебора, используйте шаблон AbstractFactory и объявите метод в интерфейсе класса фабрики, который имеет необходимые подписи.
Пройти
GraphicsDeviceManager
в качестве параметра кUpdate
а такжеDraw
методы.Используйте платформу композитно-объектно-ориентированного программирования для передачи
GraphicsDeviceManager
в ту часть объекта, которая этого требует. На мой взгляд, это довольно экспериментальное решение.
Ситуация, которую вы описываете, не легко справиться в целом. Аналогичным случаем могут быть объекты в бизнес-приложении, которым требуется доступ к базе данных.
Хотя вы не можете определить сигнатуру конструктора в интерфейсе, я чувствую, что стоит упомянуть, что это может быть место для рассмотрения абстрактного класса. Абстрактные классы могут определять нереализованные (абстрактные) сигнатуры методов так же, как интерфейс, но могут также реализовывать (конкретные) методы и конструкторы.
Недостатком является то, что, поскольку это тип класса, он не может использоваться ни для одного из сценариев с множественным типом наследования, которые может использовать интерфейс.
Я использую следующий шаблон, чтобы сделать его пуленепробиваемым.
- Разработчик, который выводит свой класс из базы, не может случайно создать общедоступный конструктор
- Разработчик финального класса вынужден пройти через общий метод create
- Все типобезопасно, отливки не требуются
- Он на 100% гибок и может использоваться повторно везде, где вы можете определить свой собственный базовый класс.
Попробуйте, вы не сможете сломать его без внесения изменений в базовые классы (кроме случаев, когда вы определяете устаревший флаг без флага ошибки, установленного в true, но даже тогда вы получите предупреждение)
public abstract class Base<TSelf, TParameter> where TSelf : Base<TSelf, TParameter>, new() { protected const string FactoryMessage = "Use YourClass.Create(...) instead"; public static TSelf Create(TParameter parameter) { var me = new TSelf(); me.Initialize(parameter); return me; } [Obsolete(FactoryMessage, true)] protected Base() { } protected virtual void Initialize(TParameter parameter) { } } public abstract class BaseWithConfig<TSelf, TConfig>: Base<TSelf, TConfig> where TSelf : BaseWithConfig<TSelf, TConfig>, new() { public TConfig Config { get; private set; } [Obsolete(FactoryMessage, true)] protected BaseWithConfig() { } protected override void Initialize(TConfig parameter) { this.Config = parameter; } } public class MyService : BaseWithConfig<MyService, (string UserName, string Password)> { [Obsolete(FactoryMessage, true)] public MyService() { } } public class Person : Base<Person, (string FirstName, string LastName)> { [Obsolete(FactoryMessage,true)] public Person() { } protected override void Initialize((string FirstName, string LastName) parameter) { this.FirstName = parameter.FirstName; this.LastName = parameter.LastName; } public string LastName { get; private set; } public string FirstName { get; private set; } } [Test] public void FactoryTest() { var notInitilaizedPerson = new Person(); // doesn't compile because of the obsolete attribute. Person max = Person.Create(("Max", "Mustermann")); Assert.AreEqual("Max",max.FirstName); var service = MyService.Create(("MyUser", "MyPassword")); Assert.AreEqual("MyUser", service.Config.UserName); }
РЕДАКТИРОВАТЬ: И вот пример, основанный на вашем примере рисования, который даже обеспечивает абстракцию интерфейса
public abstract class BaseWithAbstraction<TSelf, TInterface, TParameter>
where TSelf : BaseWithAbstraction<TSelf, TInterface, TParameter>, TInterface, new()
{
[Obsolete(FactoryMessage, true)]
protected BaseWithAbstraction()
{
}
protected const string FactoryMessage = "Use YourClass.Create(...) instead";
public static TInterface Create(TParameter parameter)
{
var me = new TSelf();
me.Initialize(parameter);
return me;
}
protected virtual void Initialize(TParameter parameter)
{
}
}
public abstract class BaseWithParameter<TSelf, TInterface, TParameter> : BaseWithAbstraction<TSelf, TInterface, TParameter>
where TSelf : BaseWithParameter<TSelf, TInterface, TParameter>, TInterface, new()
{
protected TParameter Parameter { get; private set; }
[Obsolete(FactoryMessage, true)]
protected BaseWithParameter()
{
}
protected sealed override void Initialize(TParameter parameter)
{
this.Parameter = parameter;
this.OnAfterInitialize(parameter);
}
protected virtual void OnAfterInitialize(TParameter parameter)
{
}
}
public class GraphicsDeviceManager
{
}
public interface IDrawable
{
void Update();
void Draw();
}
internal abstract class Drawable<TSelf> : BaseWithParameter<TSelf, IDrawable, GraphicsDeviceManager>, IDrawable
where TSelf : Drawable<TSelf>, IDrawable, new()
{
[Obsolete(FactoryMessage, true)]
protected Drawable()
{
}
public abstract void Update();
public abstract void Draw();
}
internal class Rectangle : Drawable<Rectangle>
{
[Obsolete(FactoryMessage, true)]
public Rectangle()
{
}
public override void Update()
{
GraphicsDeviceManager manager = this.Parameter;
// TODo manager
}
public override void Draw()
{
GraphicsDeviceManager manager = this.Parameter;
// TODo manager
}
}
internal class Circle : Drawable<Circle>
{
[Obsolete(FactoryMessage, true)]
public Circle()
{
}
public override void Update()
{
GraphicsDeviceManager manager = this.Parameter;
// TODo manager
}
public override void Draw()
{
GraphicsDeviceManager manager = this.Parameter;
// TODo manager
}
}
[Test]
public void FactoryTest()
{
// doesn't compile because interface abstraction is enforced.
Rectangle rectangle = Rectangle.Create(new GraphicsDeviceManager());
// you get only the IDrawable returned.
IDrawable service = Circle.Create(new GraphicsDeviceManager());
}
Было бы очень полезно, если бы можно было определять конструкторы в интерфейсах.
Учитывая, что интерфейс - это контракт, который должен использоваться указанным способом. Следующий подход может быть жизнеспособной альтернативой для некоторых сценариев:
public interface IFoo {
/// <summary>
/// Initialize foo.
/// </summary>
/// <remarks>
/// Classes that implement this interface must invoke this method from
/// each of their constructors.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// Thrown when instance has already been initialized.
/// </exception>
void Initialize(int a);
}
public class ConcreteFoo : IFoo {
private bool _init = false;
public int b;
// Obviously in this case a default value could be used for the
// constructor argument; using overloads for purpose of example
public ConcreteFoo() {
Initialize(42);
}
public ConcreteFoo(int a) {
Initialize(a);
}
public void Initialize(int a) {
if (_init)
throw new InvalidOperationException();
_init = true;
b = a;
}
}
Ты не
Конструктор является частью класса, который может реализовать интерфейс. Интерфейс - это просто набор методов, которые класс должен реализовать.
Один из способов заставить своего рода конструктор - объявить только Getters
в интерфейсе, что может означать, что реализующий класс должен иметь метод, в идеале конструктор, для установки значения (private
лы) за это.
Интерфейсы - это Контракт Интерфейс не допускает полей, т.е. не нужно ничего инициализировать. Интерфейс содержит только ссылку. Интерфейс должен требовать производного класса для хранения памяти.
Поздно, но кому-то нужно.
Вы можете использовать Func
void Create(Func<IDrawable> func)
{
IDrawable result = func.Invoke();
}
GraphicsDeviceManager graphicsDeviceManager =...;
xxx.Create(() => new Draw(graphicsDeviceManager));
yyy.Create(() => new Update(graphicsDeviceManager));
Если я правильно понял OP, мы хотим применить контракт, в котором GraphicsDeviceManager всегда инициализируется путем реализации классов. У меня была похожая проблема, и я искал лучшее решение, но это лучшее, что я могу придумать:
Добавьте SetGraphicsDeviceManager(GraphicsDeviceManager gdo) к интерфейсу, и таким образом реализующие классы будут вынуждены написать логику, которая потребует вызова из конструктора.
Начиная с C# 8.0, член интерфейса может объявлять тело. Это называется реализацией по умолчанию. Члены с телами позволяют интерфейсу предоставлять реализацию "по умолчанию" для классов и структур, которые не обеспечивают реализацию переопределения. Кроме того, начиная с C# 8.0, интерфейс может включать:
Константы Операторы Статический конструктор. Вложенные типы Статические поля, методы, свойства, индексаторы и события. Объявления членов с использованием явного синтаксиса реализации интерфейса. Явные модификаторы доступа (доступ по умолчанию - открытый).