Swift の秩序を歪ませる (Swift と Objective-C の連携)

異なる言語間の密な連携は、ときに矛盾が生まれてしまうものかと思います。
例えば、「定数は初期化後、再代入されない」「override 指定がないから override されていない」、その決まりは絶対でしょうか…？

今回は、挙動として個人的におもしろかったもの 3 つをピックアップしてご紹介しようと思います。

* nullablity の上書き
* `override` 指定のないメソッドが実は override されている
* `let` 宣言の変数の値が複数回変更できる

# 事例

## nullablity の上書き
まずは動作を。

```swift:動作
/* 構成
 * ParentObjcClass <- 継承 - ChildObjcClass
 */

let parent = ParentObjcClass().test() // String?
let child = ChildObjcClass().test()   // String

let cast = (ChildObjcClass() as ParentObjcClass).test()  // String?
```

`nullUnspecifiedString()` の戻り値の型が一方は `Optional` で、もう一方は `Optional` ではありません。

### 実装

```objc:ObjcClass.h
#import <Foundation/Foundation.h>

@interface ParentObjcClass : NSObject
- (null_unspecified NSString *)test;
@end

@interface ChildObjcClass : ParentObjcClass
- (nonnull NSString *)test;
@end
```

```objc:ObjcClass.m
@implementation ParentObjcClass
- (null_unspecified NSString *)test {
    return @"";
}
@end

@implementation ChildObjcClass
- (nonnull NSString *)test {
    return [super test];
}
@end
```

察しましたでしょうか。
nullablity の指定を各型で異なるものにしています。
これにより、生成される Interface は下記になります。

```swift
import Foundation

open class ParentObjcClass : NSObject {
    open func test() -> String!
}

open class ChildObjcClass : ParentObjcClass {
    open func test() -> String
}
```

### 補足
Swift でも類似した書き方で定義はできますが、メソッドを使用すると `Segmentation fault: 11` でコンパイルできませんでした…。
Objective-C の nullablity 指定による規制は Swift と違って軽いメタ情報の付与にとどまっているように思います。その違いもあり、Objective-C では許容されたのかなと感じました。

## `override` 指定のないメソッドが実は override されている

```swift
/* 構成
 * ObjcClassA <- 継承 - SwiftClassA
 * SwiftClassA 内で override 指定のメソッドは無し
 */
let child: ObjcClassA = SwiftClassA()
let parent: ObjcClassA = ObjcClassA()

parent.hogeInObjc()  // ObjcClassA.m
child.hogeInObjc()   // SwiftClassA.swift(override)
```

override 指定が無いにもかかわらず、`hogeInObjc()` が override されているかのような挙動になります。

### 実装

```objc:ObjcClassA.h
@interface ObjcClassA : NSObject

- (void)hogeInObjc;

@end
```

```objc:ObjcClassA.m
#import "ObjcClassA.h"

@implementation ObjcClassA

- (void)hogeInObjc {
    [self privateMethod];
}

- (void)privateMethod {
    NSLog(@__FILE__);
}

@end
```

```swift:SwiftClassA.swift
class SwiftClassA: ObjcClassA {
    func privateMethod() {
        print(#file+"(override)")
    }
}
```

実は、実際に override されているのは、`func hogeInObjc()` ではなく、`func privateMethod()` でした。
しかし、`class SwiftClassA` 内に `override` キーワードはありません。
`func privateMethod()` は親クラス (`class ObjcClassA`) の interface にないため、Swift のコンパイラから見えていないようです。動作的に、実行時に動的に紐づけられている？のでしょうか(´・ω・｀)？

ちなみに、Swift だとこの書き方はできません。
Swift の場合、アクセス修飾子によって完全に可視性が制限されており、`func privateMethod` は override されません。(下記参照)

```swift
fileprivate class A {
    func hoge() { privateMethod() }
    private func privateMethod() {
        print("test")
    }
}

fileprivate class B: A {
    //    func hoge() {}  // need override
    func privateMethod() {
        print("override")
    }
}

```

```swift
let a = A()
let b = B()

a.hoge()  // "test"
b.hoge()  // "test"
```


## `let` 宣言の変数の値が複数回変更できる

とってもトリッキーですが、ご紹介まで。

```swift
/* 構成
 * UIViewController <- 継承 - BaseViewController <- 継承 - HogeViewController
 */
class HogeViewController: BaseViewController {
    
    private let v: String
    
    init(v: String) {
        self.v = "\(#line)" // ... ①
        print("before v: \(self.v)")  // before v: 16
        super.init(num: 1)  // ... ②
        print("after v: \(self.v)")   // after v: 23
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        self.v = "\(#line)"
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    // 中略
}
```

`let v` に対して ① で値を設定しています。しかし、② を実行後に `let v` の値が変化しています。

## 実装

以下に全コードを載せますが、長いので挙動の概要を説明します。

1. `#HogeViewController.init(v: String)` を呼ぶ
2. 内部で親クラス (`BaseViewController`) のイニシャライザ (`initWithNum:(int)num`) を呼ぶ
3. **暗黙的に** `init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)` が呼ばれる
4. `v` に値が再代入される (③)

```objc:BaseViewController.h
#import <UIKit/UIKit.h>

@interface BaseViewController : UIViewController

- (instancetype)initWithNum:(int)num;

@end
```

```objc
#import "BaseViewController.h"

@interface BaseViewController ()

@end

@implementation BaseViewController

- (instancetype)initWithNum:(int)num {
    self = [super init];
    return self;
}

@end
```

```swift
import UIKit

class HogeViewController: BaseViewController {
    
    private let v: String
    
    init(v: String) {
        self.v = "\(#line)"  // ... ①
        print("before v: \(self.v)")  // before v: 16
        super.init(num: 1)  // ... ②
        print("after v: \(self.v)")   // after v: 23
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        self.v = "\(#line)"  // ... ③
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
```

## 補足
この現象の元凶は、Objective-C で書いた `BaseViewController` がイニシャライザ規約違反をしていることです。
`- (instancetype)initWithNum:(int)num` で designated イニシャライザを呼ばなければいけないところを、呼ばすに初期化を完了させています。(今回の現象に関係ないのですが `required init?(coder aDecoder: NSCoder)` の実装がないことも違反です)

Swift ではイニシャライザの規約を遵守しているか、言語 (及びコンパイラ) が保証してくれるようになりましたよね。
しかし、Objective-C はエンジニア任せでした。つまり、規約に違反していても実行可能でした。
今回は規約違反をすることで、イニシャライザの呼び出しフローを変えることができました。(本来なら `HogeViewController.init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)` は呼ばれないはず)

これは、実際に Objective-C と Swift の混在するプロジェクトで遭遇しうる問題だと思いますので、気をつけて見てあげた方が良いかもしれません。

# まとめ
今回は、Swift と Objective-C の違いの一部である、下記を利用して歪みを生み出してみました。

* nullablity の言語的サポート
* メソッドやプロパティ等の可視性の違い
* イニシャライザの定義ルールの遵守に対する言語的サポートの有無

こういった、言語間の違いから考えてみて、秩序を歪ませられないか突ついてみると楽しい気がします ( 'ω')