Drop NS Prefix in Swift Foundation
01 何が問題だったのか
Foundation は Objective-C の時代から Apple プラットフォームの中核をなすライブラリで、NSDate、NSURL、NSBundle、NSFileManager、NSOperation のように、主要な型名はいずれも NS プレフィックスを持っていました。これは、Objective-C がモジュールごとの名前空間を持たず、グローバルなシンボル同士の衝突を避けるためにクラス名へベンダー固有のプレフィックスを付ける慣習があったためです。
一方、Swift ではモジュールが名前空間として機能するため、型名に Objective-C 由来のプレフィックスを付ける必要がありません。実際、Swift 標準ライブラリは Array、String、Dictionary のようにプレフィックスを持たない、簡潔で Swift 流の型名で構成されています。
Swift 3 のネーミング方針とのずれ
Swift 3 では、API 設計ガイドラインの整備(SE-0023)や標準ライブラリへのガイドライン適用(SE-0006)、Objective-C API のインポート時の名前変換改善(SE-0005)といった一連の取り組みによって、「明確で、簡潔で、不要な語やプレフィックスを省いた」ネーミングへと舵が切られました。
この流れの中で Swift Core Libraries(swift-corelibs-xctest / swift-corelibs-dispatch / swift-corelibs-foundation)を「Swift 本来のライブラリ」として位置づけ直す動きが進んでおり、Foundation だけが NS プレフィックスを引きずっていると、標準ライブラリや SDK の他の部分との間に「異物感」が残り続けます。たとえば Swift の世界では次のような混在が目立ちます。
let manager = NSFileManager.defaultManager()
let url = NSURL(fileURLWithPath: "/tmp/foo")
let bundle = NSBundle.mainBundle()
// 標準ライブラリや新しい SDK 側は String / Array のようにプレフィックスなし
どこまで落とすかが自明でない
では「Foundation の型から一律に NS を外せばよい」かというと、そう単純でもありません。Foundation には次のように性格の異なる型が混在しています。
- Objective-C ランタイムや
NS名前空間そのものと強く結びついた型(NSObject、NSAutoreleasePool、NSExceptionなど) - 実質的に AppKit / UIKit 側の世界に属する、プラットフォーム固有の型(
NSUserNotification、NSXPCConnectionなど) - SE-0069 で値型版(
struct)が用意された、あるいは近い将来用意される型(NSArray、NSString、NSAttributedString、NSRegularExpressionなど) - 標準ライブラリの既存の型と名前が衝突する型(
NSTaskに対する標準ライブラリのProcessなど)
加えて、関連する列挙体や定数が NSDateFormatterStyle.NSDateFormatterShortStyle のようにクラス名を重ねた冗長な形で公開されているケースも多く、型名だけでなくそうしたサブタイプや定数群も同時に整理しないと、中途半端に Swift らしくない API が残ってしまいます。
Foundation の API を Swift 3 のネーミングに沿った形に整える上で、「どの型からは NS を外し、どの型はあえて残すのか」「外すと決めた型について、関連する列挙体や定数をどう整理するのか」を、Swift 側の利用者にとって一貫した基準で決める必要がありました。
02 どのように解決されるのか
Foundation の多くの型から Swift 上での NS プレフィックスを外し、あわせて関連する列挙体や定数を Swift らしい形に整理します。Swift 3.0 で実装されています。変更は Swift から見たときの名前だけ に関するもので、Objective-C 側の API には影響しません。
NS を外すかどうかのルール
今後新しく Foundation に追加される型を含めて、次の基準で判断します。
- Objective-C ランタイムや
NS名前空間そのものと結びついた型は、NSを残します。例:NSObject、NSAutoreleasePool、NSException、NSProxy。 - 実体が AppKit / UIKit など上位フレームワークの領域に属するプラットフォーム固有の型は、それらのフレームワークがプレフィックスを維持しているのに合わせて、
NSを残します。例:NSUserNotification、NSBackgroundActivity、NSXPCConnection。 - SE-0069 によって値型版が用意されている型は、クラス側に
NSを残します。値型側がDateやDataのようにプレフィックスなしの名前を担当し、クラス側のNSDate/NSDataは引き続き存在します。
既存の型についてはさらに次のルールを加えます。
- 近い将来に値型版が追加される見込みの型は、先回りして
NSを残します。例:NSAttributedString、NSRegularExpression、NSPredicate。 NSLock系のロック用クラス・プロトコルは、今後の並行処理の整理の中で見直されることが見込まれるためNSを残します。NSCache、NSMapTable、NSHashTable、NSOrderedSetのような、オブジェクト限定のコレクション系は、将来的にstruct化される可能性があるためNSを残します。- 単にプレフィックスを外すだけでなく、役割を表すより適切な名前に改名する型もあります。例:
NSTaskはProcessへ。
これらのルールに当てはまらない型は、Swift 上での名前から NS が外れます。
代表的な改名
たとえば次のような型が、Swift から見える名前を変えます(いずれも Objective-C 側の NSBundle などの名前はそのまま)。
| Objective-C 名 | Swift での名前 |
|---|---|
NSBundle |
Bundle |
NSFileManager |
FileManager |
NSNotificationCenter |
NotificationCenter |
NSOperation / NSOperationQueue |
Operation / OperationQueue |
NSURLSession 系 |
URLSession 系 |
NSUserDefaults |
UserDefaults |
NSDateFormatter / NSNumberFormatter |
DateFormatter / NumberFormatter |
NSJSONSerialization |
JSONSerialization |
NSRunLoop / NSThread / NSTimer |
RunLoop / Thread / Timer |
NSTask |
Process |
NSXMLParser 系 |
XMLParser 系 |
Objective-C 時代のコードが次のように書き換わるイメージです。
// 従来
let manager = NSFileManager.defaultManager()
let formatter = NSDateFormatter()
let center = NSNotificationCenter.defaultCenter()
// Swift 3 以降
let manager = FileManager.default
let formatter = DateFormatter()
let center = NotificationCenter.default
NSTask から Process への改名は、標準ライブラリに既存の Process 型(プログラムの起動引数などを取得する enum)があるため、そちらを廃止して ProcessInfo に統合した上で、Foundation 側が Process の名前を引き取る、という整理で実現されています。
また NSOutputStream は OutputStream になりますが、これも標準ライブラリの既存の OutputStream プロトコル(テキスト出力先を表す型)と名前が衝突します。そちらは TextOutputStream へ改名され、空いた OutputStream の名前を Foundation の新しい入出力ストリーム型が担当します。
入れ子になった列挙体・定数の整理
Objective-C 流の NSDateFormatterShortStyle や NSJSONReadingMutableContainers のような、クラス名を重ねた列挙体・定数は、対応する Swift 型の中に入れ子にした上で、名前から重複部分を落とします。
// 従来(イメージ)
let style: NSDateFormatterStyle = .NSDateFormatterShortStyle
let options: NSJSONReadingOptions = .MutableContainers
// Swift 3 以降
let style: DateFormatter.Style = .short
let options: JSONSerialization.ReadingOptions = .mutableContainers
たとえば DateFormatter.Style は .none / .short / .medium / .long / .full の 5 つのケースを持つ入れ子の enum になり、NSComparisonPredicate 関連の NSComparisonPredicateOptions や NSPredicateOperatorType は、それぞれ ComparisonPredicate.Options / ComparisonPredicate.Modifier / ComparisonPredicate.Operator のような入れ子型として整理されます。XMLDocument.ContentKind、XMLNode.Kind、XMLDTDNode.Kind のように XML 関連の Kind 系も同じ方針です。
こうした入れ子化により、
DateFormatter.Style.short
JSONSerialization.ReadingOptions.mutableContainers
ComparisonPredicate.Operator.beginsWith
のように「どの型に属する設定なのか」が型名から自然に読み取れるようになります。
NSStringEncoding と String.Encoding
NSStringEncoding は、NSUTF8StringEncoding / NSASCIIStringEncoding などの UInt 定数が並ぶだけの typealias として公開されていました。これを String.Encoding という RawRepresentable な struct に作り直し、各エンコーディングはその static メンバとして提供します。
// 従来
let encoding: NSStringEncoding = NSUTF8StringEncoding
// Swift 3 以降
let encoding: String.Encoding = .utf8
let s = String(data: data, encoding: .utf8)
let d = "hello".data(using: .utf8)
これにより、「エンコーディングを表す型」と「具体的なエンコーディング値」の関係が、他の Swift の enum / OptionSet 型と揃った形で表現されます。
プレフィックスを残す型の使い方
NSObject、NSAttributedString、NSRegularExpression、NSPredicate、NSCache、NSLock、NSXPCConnection のように NS が残る型は、Swift からも従来どおり NS 付きの名前で利用します。これらは前述のいずれかのルール(Objective-C ランタイムと強く結びつく、プラットフォーム固有、値型版の追加が見込まれる、将来的な再設計が想定される、など)に当てはまるため、名前を動かさないほうが長期的に一貫すると判断されたものです。
NSArray / NSString / NSDictionary も同様で、Swift から日常的に使うのは値型の Array / String / Dictionary であり、Objective-C の API を経由するときに NSArray などの名前がそのまま見えるという関係になっています。
既存コードへの影響
Swift 側の型名が大きく変わるため、この提案に対応したツールチェインでは、既存のすべての Swift プロジェクトが新しい名前へのマイグレーションを必要とします。Xcode のマイグレータや Fix-it が古い名前を新しい名前に書き換える形で移行が進められました。Objective-C 側のコードには変更はありません。
今日の Swift で当たり前に目にする Bundle、FileManager、URLSession、NotificationCenter、DateFormatter.Style.short、String.Encoding.utf8 といった型名は、この提案で整理された結果として成立しています。