Edited at
iOSDay 11

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

More than 1 year has 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 の言語的サポート

  • メソッドやプロパティ等の可視性の違い

  • イニシャライザの定義ルールの遵守に対する言語的サポートの有無

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