TL; DR: речь идет о переносе шаблона Objective-C в Swift. Возможно, лучше сначала взглянуть на интерфейс Objective-C ниже, чтобы лучше понять, чего я пытаюсь достичь.
Я только начинаю адаптировать довольно большую кодовую базу из Objective-C в Swift. В унаследованной кодовой базе были заложены некоторые шаблоны проектирования, чтобы попытаться обеспечить некоторую безопасность типов.
Эти шаблоны кажутся действительно неуместными в Свифте, но я не уверен, каков правильный «Свифт Путь» для этого. Использование Generics похоже на то, как это сделать, но мне неясно, как лучше поступить.
Цель состоит в том, чтобы создать структуру, обладающую свойством, которое может содержать «почти все». Вызывающие ожидают, что свойство будет иметь определенный тип при использовании, и в случае несоответствия типов должно быть выдано сообщение об ошибке или исключении. (т. е. вызывающая сторона ожидала, что аргумент будет целым числом, но в действительности строка была сохранена.)
struct Command<T> {
let directive: Directive
let argument: T
}
let command = Command(directive: .draw, argument: NSZeroRect)
let command2 = Command(directive: .toggle, argument: true)
// Somewhere else in the code...
//
// How do I pass in a Command<> here?
// This generates an error because Command<Bool> cannot be converted to Command<Any>
//
func processCommand(_ command:Command<Any>) {
switch command.directive {
case .draw:
// How do I ensure that command.argument is indeed an NSRect?
case .toggle:
// How do I ensure that command.argument is indeed a boolean?
}
}
Интерфейс Objective-C выглядит примерно так. Обратите внимание, что аргумент может быть разных типов. Начиная от примитивов (целых, логических, двойных и т. Д.), До всего, что может храниться в NSValue или поддерживает NSCoding.
Существует несколько методов доступа к свойствам для каждого типа, где это имеет смысл.
@interface FLCommand : NSObject
@property(assign, readonly) FLDirective directive;
@property(strong, readonly) id argument;
@property(strong, readonly) BOOL argumentAsBoolean;
@property(strong, readonly) NSRect argumentAsRect;
- (instancetype)initWithDirective:(FLDirective)directive booleanArgument:(BOOL)value;
- (instancetype)initWithDirective:(FLDirective)directive rectArgument:(NSRect)rect;
- (instancetype)initWithDirective:(FLDirective)directive argument:(id)arg;
@end
@implementation FLCommand
- (instancetype)initWithDirective:(FLDirective)directive
booleanValue:(BOOL)value {
// Convert boolean to object.
return [self initWithDirective:directive
argument:@(value)];
}
- (instancetype)initWithDirective:(FLDirective)directive
rectArgument:(NSRect)rect {
// Convert NSRect to object.
return [self initWithDirective:directive
argument:[NSValue valueWithRect:rect]];
}
- (BOOL)argumentAsBoolean {
NSAssert([_argument isKindOfClass:NSNumber.class], @"Expected argument to be an NSNumber.");
return [self.argument boolValue];
}
- (NSRect)argumentAsRect {
NSAssert([_argument isKindOfClass:NSValue.class], @"Expected command argument to be an NSValue.");
return [(NSValue *)self.argument rectValue];
}
@end
// Somewhere else in the code the commands are acted upon. Using the
// asserts and type-specific property accessors offers a poor-man's
// way of doing type safety to ensure the the command's argument is
// of the expected type.
- (void)processCommand:(FLCommand *)command {
switch (command.directive) {
case FLDirectiveToggleSomething:
// The assert will fire if the argument is not a boolean.
[self toggleSomething:command.argumentAsBoolean];
break;
case FLDirectiveDrawSomething:
[self drawSomethingInFrame:command.argumentAsRect];
break;
}
}
}
Использование эквивалентного паттерна в Swift кажется мне не слишком быстрым, как у меня. Есть ли лучший способ сделать это с помощью Generics?
С решениями Swift 5 и macOS 10.15+ все в порядке.