Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

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

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

事例

nullablity の上書き

まずは動作を。

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

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

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

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

実装

ObjcClass.h
#import <Foundation/Foundation.h>

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

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

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

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

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 されている

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

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

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

実装

ObjcClassA.h
@interface ObjcClassA : NSObject

- (void)hogeInObjc;

@end
ObjcClassA.m
#import "ObjcClassA.h"

@implementation ObjcClassA

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

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

@end
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 されません。(下記参照)

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

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

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

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

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

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

/* 構成
 * 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 に値が再代入される (③)
BaseViewController.h
#import <UIKit/UIKit.h>

@interface BaseViewController : UIViewController

- (instancetype)initWithNum:(int)num;

@end
#import "BaseViewController.h"

@interface BaseViewController ()

@end

@implementation BaseViewController

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

@end
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 の言語的サポート
  • メソッドやプロパティ等の可視性の違い
  • イニシャライザの定義ルールの遵守に対する言語的サポートの有無

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away