Objc-C для Swift: Как создать свойство в Swift, которое гарантирует, что оно будет определенного типа, когда вызывающие используют его? - PullRequest
1 голос
/ 09 июня 2019

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+ все в порядке.

1 Ответ

2 голосов
/ 09 июня 2019

Рассматривали ли вы использование перечислений со связанными значениями (часто называемыми сложными перечислениями)

enum Directive {
    case draw(NSRect)
    case toggle(Bool)
}

struct Command {
    let directive: Directive
}

let command = Command(directive: .draw(.zero))
let command2 = Command(directive: .toggle(true))

func processCommand(_ command: Command) {
    switch command.directive {
    case .draw(let rect):
        // do something with rect
    case .toggle(let value):
        // do something with the value
    }
}

(И вы могли бы вообще пропустить структуру Command в приведенном выше)

Или альтернативное решение - использовать протокол со связанным типом:

protocol Command {
    associatedtype AssociatedType

    var argument: AssociatedType { get }

    init(_ argument: AssociatedType)

    func process()
}

struct DrawCommand: Command {
    typealias AssociatedType = NSRect
    let argument: AssociatedType

    init(_ argument: AssociatedType) {
        self.argument = argument
    }

    func process() {
        print("draw something with \(argument)")
    }
}

struct ToggleCommand: Command {
    typealias AssociatedType = Bool
    let argument: AssociatedType

    init(_ argument: AssociatedType) {
        self.argument = argument
    }

    func process() {
        print("toggle something with \(argument)")
    }
}

let command = DrawCommand(.zero)
let command2 = ToggleCommand(true)

command.process()
command2.process()

Это немного более шаблонно / перегружено, но обеспечивает лучшее разделение проблем и будет более гибким для вас, чтобы вы могли вводить больше команд в будущем без необходимости обновлять enum / switch в нескольких местах кода.

...