代码之家  ›  专栏  ›  技术社区  ›  kennyc

Objc-C到Swift:如何在Swift中创建一个属性,以保证调用方使用它时它是特定类型的?

  •  1
  • kennyc  · 技术社区  · 5 年前

    我刚刚开始将一个相当大的代码库从Objective-C修改为Swift。在遗留代码库中有一些设计模式是为了尝试提供某种类型安全性。

    目标是创建一个结构,其属性可以容纳“几乎任何东西”。调用方希望属性在使用时为特定类型,如果类型不匹配,则应引发错误或异常。(即:调用者希望参数是整数,但实际上存储了字符串。)

    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?
      }
    }
    

    每种类型都有多个有意义的属性访问器。

    @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;
            }
        }
    }
    

    1 回复  |  直到 5 年前
        1
  •  2
  •   Claus Jørgensen    5 年前

    您是否考虑过使用具有关联值的枚举(通常称为复杂枚举)

    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()