TAKASHINGS BLOG

仕事のこととか日記とか。映画大好きです。

「現場のためのSwift4」の訂正・補足

「現場のためのSwift4」(以下、本書)の発売から2週間あまり経ちました。 多くの書店様へご挨拶へ伺うと、複数冊置いていただいたり、書店様によっては「売れていますよ」と教えていただくこともありました。

ご購入いただきました皆様本当にありががとうございます。

現場のためのSwift4 Swift4.1+Xcode9.3対応

現場のためのSwift4 Swift4.1+Xcode9.3対応

また、何人かの方に書評も書いていただきました。 この場を借りて、御礼申し上げます。

blog.personal-factory.com

egg-is-world.com

medium.com

ここからが本題ですが『「現場のためのSwift4」を読んだ by @takasek』の記事にていくつかご指摘をいただきました。

今回はこのご指摘いただいている項目の一部の補足説明をさせていただきたいと思います。 なお、今回訂正・補足の対象は記事内の「nilについて」「継承について」の2点となります。

この2点を取り扱ったのは以下の理由からです。

  • Swiftでは避けて通れない「nil」に対して本書での書き方に不明瞭である部分があったこと
  • 継承で取り扱っている「BaseViewControllerの設計」は現場においてアンチパターンである場合があること
  • また、そのアンチパターンに対する代替案を(個人的に)提示すべきだと判断したため

nilについて

第6章:6-3 nilについて 138P

▼定数の定義時にnil を代入しようとしてもエラーが発生する
let str: String = nil

そもそも、nilは「存在しない」という意味があります。コンパイラは存在しないものを操作しようとすると、どうしていいかわかりません。そのため、コンパイラのレベルでnilの操作を行うとエラーが発生します。

ここで押さえておくべきことは「nilに対する操作はクラッシュする」ということであり、Swiftではコンパイラがnilの扱いを厳密にチェックしているということです。

上記の文章に対して記事内にて以下のご指摘をいただきました。

nilの操作」が何を指すのか不明ですが、少なくともクラッシュすることはありません。

この点について、補足説明いたします。 まず「nilの操作」について補足説明をいたします。「nil」をあたかもコントロールできるように受け取りかねない書き方となってしまったこと、そして適切ではない表現だったことをお詫びいたします。この箇所を訂正する場合「nilへのアクセス」という言葉が適切ではないかと考えています。

冒頭のコード「let str: String = nil」ですが、この一文でアプリがクラッシュすることはありません。 nilを許容しない非Optional型にnilを代入しようとする処理(コード)に対して、ビルドをする前の段階でコンパイラーがエラーを出します。

では、どのような場合にクラッシュするのか。 それはプログラム(アプリ)実行時に発生するランタイムエラーを検出した際にクラッシュします。

import UIKit

// viewDidLoadなどで実行
var myLabel: UILabel!      // nilを許容するOptional型で定義 
myLabel.text = "label"      // ここでエラー

上記のコードでは、コンパイル時にはエラーは検出されません。

プログラム実行時、nilを許容するUILabelの変数・myLabelが初期化されないまま(nilの状態)、「myLabel.text = "label"」で文字列を設定しようとしたため、ランタイムエラーが発生します。

なお、ランタイムエラーはnilにへのアクセスを行おうとする以外にも、型変換を行おうとした際に正常に型変換が行うことができなかった場合なども同様にランタイムエラーになります。

まとめると、nilを許容しない変数へのnilの代入は、ビルド前の段階でコンパイルエラーが発生。 また、コンパイル時にはエラーとならず、プログラム実行時に意図的に(または意図せず)nilへのアクセスを行おうとすると、ランタイムエラーが発生し、アプリがクラッシュします。

○継承について

第7章:継承と拡張 238P

class BaseViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 戻るのテキストを設定
        navigationItem.backBarButtonItem = UIBarButtonItem(title: "戻る", style: UIBarButtonItemStyle.done, target: nil, action: nil)
    }
}

本書内で例を挙げた上記の「BaseViewController」の設計の採用はデメリットが多い、BaseViewControllerの設計を採用すべきではないとのご指摘をいただきました。以下、@takasekさんの記事から引用させていただきます。

継承という仕組み自体の解説としては間違っていないのですが、BaseViewControllerという設計にはデメリットが多いという議論もされています。私もBaseViewControllerは使うべきではないと思っています。なぜなら、こういった処理の共通化のための継承は、例外的なサブクラスの存在を許さず、変更に極端に弱いコードになってしまいます。また責務が不明確なため、容易に肥大化の道を辿り手がつけられなくなります。

「BaseViewController」の設計は共通化を目的にするあまりに肥大化してしまう可能性が高く、細かい変更に対応できずに、「BaseViewControllerA」「BaseViewControllerB」などの派生クラスを作成しかねない、メリットを十分に活かせずにデメリットとなってしまいます。

そのため、「BaseViewController」の一例が「現場」という観点では、今回の継承の例として挙げる例では不適切でした。お詫び申し上げます。

では、どのようにするのが好ましいでしょうか。 一例として、プロトコルを用いた実装が挙げられます。

Swiftには「Protocol-Oriented Programming」(プロトコル指向)という思想・考え方があります。 大まかに言うと「継承によって型の性質を定義するのではなく、プロトコルを用いて型の性質を定義する」ものです。

継承で使用した「BaseViewController」の戻るボタンを共通化する場合を例にしてみたいと思います。

// プロトコルの指定。ここには具体的な実装は行わず、関数、変数名のみ記載
protocol NavigationBackButtonProtcol {
    func setBackButton()
}

// プロトコルを指定
class MainViewController: UIViewController, NavigationBackButtonProtcol {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 戻るのテキストを設定
        setBackButton()
    } 
    
    // プロトコルで指定した関数の具体的な実装を行う
    func setBackButton() {
        navigationItem.backBarButtonItem = UIBarButtonItem(title: "戻る", style: UIBarButtonItemStyle.done, target: nil, action: nil)
    }
}

上記のように、プロトコルには定義のみを書き、それをクラス、構造体、列挙型に指定して実装を行います。

継承は親クラスに指定した変数や関数全てが継承されます。仮に使わない変数や関数があったとしてもです。 プロトコルでの実装では、プロトコルごとに性質(変数や関数など)を決め、それをクラスに指定することができます。

継承と異なる点は、親クラスによってクラスの性質を決めるのではなく、プロトコルによって付けたい機能や性質などを決めるという点です。

また、プロトコルの内容を共通化したいのであれば、以下のようにextensionで具体的な実装を行うことで各クラスごとに実装内容を書く必要がなくなり、プロトコルの指定と関数の実行のみを書くことで実現できます。

// プロトコルに書いた関数の具体的な処理を記述
extension NavigationBackButtonProtcol where Self: UIViewController {
    func setBackButton() {
        navigationItem.backBarButtonItem = UIBarButtonItem(title: "戻る", style: UIBarButtonItemStyle.done, target: nil, action: nil)
    }
}

// プロトコルを指定
class MainViewController: UIViewController, NavigationBackButtonProtcol {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // 戻るのテキストを設定
        setBackButton()
    } 
}

簡単ではありますが「BaseViewController」の例と似た形での実装方法をご紹介しました。

なお「BaseViewController」を用いた継承の設計は間違いではありませんが、場合によってアンチパターンとなり得ます。

今回は「BaseViewController」に代わる手法の紹介を重視したため、具体的なプロトコル指向について、継承と異なる詳細な相違点などは割愛しました。 気になる方はぜひチェックしてみてください。

以上となります。 最後に@takasekさんの詳細な例を踏まえたご指摘ありがとうございました。

なお、今回いただいたご指摘の他に、本書内の一部内容に誤りが記載されていたため、正誤表を掲載しています。正誤表は下記のリンクからご確認いただけます。 ご購入いただきました皆様は下記のリンクからご確認をお願いいたします。

www.shuwasystem.co.jp