Как написать абстрактный конструктор класса, чтобы он был гибким для расширения в подклассах
Я пытаюсь реализовать постоянный Stack
структура данных. Я хочу реализовать это как алгебраический тип данных, поэтому он имеет два конкретных подтипа: пустой и непустой:
abstract class Stack<T> {
factory Stack.empty() => const _EmptyStack._();
T get data;
Stack<T> get bottom;
bool get isEmpty;
Stack<T> put(T item) => new _StackImpl(item, this);
}
class _StackImpl<T> extends Stack<T> {
final T _data;
final Stack<T> _bottom;
_StackImpl(T this._data, Stack<T> this._bottom);
T get data => _data;
Stack<T> get bottom => _bottom;
bool get isEmpty => false;
}
class _EmptyStack<T> extends Stack<T> {
const _EmptyStack._();
T get data => throw new CollectionIsEmpty();
Stack<T> get bottom => throw new CollectionIsEmpty();
bool get isEmpty => true;
}
Этот код вызывает две ошибки в конкретных реализациях:
[error] The class 'Stack' does not have a default generative constructor
Я нашел пример кода, который, кажется, решает эту проблему здесь, поэтому я исправил его, поместив конструктор без параметров в Stack<T>
учебный класс:
abstract class Stack<T> {
Stack();
// ...
но теперь это вызывает проблемы с _EmptyStack<T>
конструктор, который является постоянным:
Constant constructor cannot call non-constant super constructor of 'Stack<T>'
Дополнительно добавил Stack()
Конструктор не позволяет использовать класс в качестве mixin.
Похоже, что эти ограничения заставляют автора думать о том, как будет расширен класс. Способ расширения List
класс от dart:collection
пакет, кажется, подтверждает этот вывод - для расширения есть целый отдельный класс, я не могу напрямую расширить List
сам класс.
Мой вопрос более общий, чем проблема, описанная выше: как я могу написать класс, чтобы он мог быть достаточно гибким для расширения? Это включает в себя разрешение на использование таких функций, как:
- фабричные конструкторы в суперклассе
- нормальные конструкторы в подклассе
const
конструкторы в подклассе- использоваться как миксин
Хотя я понимаю, что использование в качестве миксина может быть невозможным или даже нежелательным, другие пункты по-прежнему актуальны. Наиболее важный вопрос стоит: почему я не могу extend
класс с фабричным конструктором? Это поведение отличается от любого другого языка OO, с которым я знаком.
Также связанные вопросы:
РЕДАКТИРОВАТЬ: Благодаря Günter Zöchbauer ответ я улучшил код, так что теперь он полностью работоспособен (см. Ниже). Самый важный вопрос, который я сейчас оставил: почему фабричный конструктор лишает возможности расширять класс? И как обойти это (кроме использования базового класса в качестве интерфейса)? Более простой пример, чтобы сделать точку:
class Base {
}
class _Sub extends Base {
int someValue;
_Sub(int this.someValue);
}
Все хорошо с этим кодом. Но скажем, я вернусь к своему Base
класс во времени и хотите добавить фабричный метод:
class Base {
factory Base.empty() => new _Sub(0);
}
Теперь каждый класс, который расширяется Base
сломан из-за unresolved implicit call to super constructor
, Что мне тогда делать?
Исправлен код из оригинального вопроса для справки:
abstract class Stack<T> {
const Stack._();
factory Stack.empty() => const _EmptyStack._();
T get data;
Stack<T> get bottom;
bool get isEmpty;
Stack<T> put(T item) => new _StackImpl(item, this);
}
class _StackImpl<T> extends Stack<T> {
final T _data;
final Stack<T> _bottom;
_StackImpl(T this._data, Stack<T> this._bottom) : super._();
T get data => _data;
Stack<T> get bottom => _bottom;
bool get isEmpty => false;
}
class _EmptyStack<T> extends Stack<T> {
const _EmptyStack._() : super._();
T get data => throw new CollectionIsEmpty();
Stack<T> get bottom => throw new CollectionIsEmpty();
bool get isEmpty => true;
}
void main(){
group('stack', (){
test('empty stack', (){
var emptyStack = new Stack.empty();
expect(emptyStack.isEmpty, isTrue);
expect(() => emptyStack.data, throwsA(new isInstanceOf<CollectionIsEmpty>()));
expect(() => emptyStack.bottom, throwsA(new isInstanceOf<CollectionIsEmpty>()));
var emptyStack2 = new Stack.empty();
expect(emptyStack == emptyStack2, isTrue);
});
test('adding to stack', (){
var stack = new Stack<String>.empty().put("a").put("b").put("c");
expect(stack.data, equals('c'));
expect(stack.bottom.data, equals('b'));
expect(stack.bottom.bottom.data, equals('a'));
});
});
}
1 ответ
В вашем примере я предлагаю просто использовать Stack
в качестве интерфейса вместо базового класса.
фабричные конструкторы в суперклассе Если у вас есть фабричный конструктор, вам также нужно добавить нормальный конструктор, если вы хотите расширить его, как указано в ответе на связанный вопрос.
нормальные конструкторы в подклассе Каков был реальный вопрос здесь? Я думаю, это то же самое, что 1.
Конструкторы const в подклассе Если вы хотите использовать конструктор const, все подклассы также должны иметь конструктор const. В классе с конструктором const все поля должны быть окончательными. Это не относится к вашему базовому классу, поэтому где смысл добавлять конструктор const к
_EmptyStack
,использоваться в качестве миксина Ограничения для классов, используемых в качестве миксина, являются временными и должны быть сняты в какой-то момент.