異なる言語間の密な連携は、ときに矛盾が生まれてしまうものかと思います。
例えば、「定数は初期化後、再代入されない」「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
ではありません。
実装
#import <Foundation/Foundation.h>
@interface ParentObjcClass : NSObject
- (null_unspecified NSString *)test;
@end
@interface ChildObjcClass : ParentObjcClass
- (nonnull NSString *)test;
@end
@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 されているかのような挙動になります。
実装
@interface ObjcClassA : NSObject
- (void)hogeInObjc;
@end
#import "ObjcClassA.h"
@implementation ObjcClassA
- (void)hogeInObjc {
[self privateMethod];
}
- (void)privateMethod {
NSLog(@__FILE__);
}
@end
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
の値が変化しています。
実装
以下に全コードを載せますが、長いので挙動の概要を説明します。
-
#HogeViewController.init(v: String)
を呼ぶ - 内部で親クラス (
BaseViewController
) のイニシャライザ (initWithNum:(int)num
) を呼ぶ -
暗黙的に
init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
が呼ばれる -
v
に値が再代入される (③)
#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 の言語的サポート
- メソッドやプロパティ等の可視性の違い
- イニシャライザの定義ルールの遵守に対する言語的サポートの有無
こういった、言語間の違いから考えてみて、秩序を歪ませられないか突ついてみると楽しい気がします ( 'ω')