- 投稿日:2020-02-10T23:15:14+09:00
31. Next Permutation
I did't know what next permutation really is but I have found some explanations on Wikipedia, and it helped.
Quick Explanation
The explanation below is grab from next permutation section on Wikipedia.
cf. https://en.wikipedia.org/wiki/Permutation#Generation_in_lexicographic_order
- Find the largest index
k
such thata[k]
<a[k+1]
. If no such index exist, the permutation is the last permutation.- Find the largest index
l
greater thank
such thata[k]
<a[l]
.- Swap the value of
a[k]
with that ofa[l]
.- Reverse the sequence from
a[k+1]
up to and including th final elementa[n]
.Other Resources
- https://www.youtube.com/watch?v=quAS1iydq7U
- Good resource to understand how it works.
Code
class Solution { func nextPermutation(_ nums: inout [Int]) { // I have created methods for each step let k = findLargestK(&nums) // ① if k >= 0 { let l = findLargestL(&nums, k) // ② swap(&nums, k, l) // ③ } reverse(&nums, k + 1) // ④ } /// Find the largest index k such that a[k] < a[k+1]. /// If no such index exist, the permutation is the last permutation. private func findLargestK(_ nums: inout [Int]) -> Int { // We start from the back because we need to find a largest index. // -2 for we need an extra space for k+1 var k = nums.count - 2 while k >= 0 && nums[k] >= nums[k+1] { k -= 1 } return k } /// Find the largest index l greater than k such that a[k] < a[l]. private func findLargestL(_ nums: inout [Int], _ k: Int) -> Int { // We start from the back because we need to find a largest index. var l = nums.count - 1 while l >= 0 && nums[k] >= nums[l] { l -= 1 } return l } private func swap(_ nums: inout [Int], _ a: Int, _ b: Int) { let temp = nums[a] nums[a] = nums[b] nums[b] = temp } private func reverse(_ nums: inout [Int], _ start: Int) { var lower = start var upper = nums.count - 1 while lower < upper { swap(&nums, lower, upper) lower += 1 upper -= 1 } } }
- 投稿日:2020-02-10T23:12:16+09:00
今からスマホアプリの学習をしようとしてる人に向けて!
1. はじめに
私は新卒でITベンチャーに入社して、現在スマホアプリのフロントエンドエンジニア(※以下 アプリのフロント)として、
Android、iOS共に、主にECアプリなどを開発しております。
そんな私がスマホアプリの学習をしようか検討中の方に向けて、「アプリのフロントはこんな感じ」というのを共有させていただければと思い記事にしました。2. スマホ開発の仕事はある?
学習するか悩んでる言語がある方の多くは、「その言語を学んでも仕事が無いんじゃ意味がない」と思っている方だと思います。
確かに「仕事がない」=「必要とされていない」と言っていいと思うので、これから学習する言語は仕事が多く取れる言語が良いですよね。
その点、スマホのエンジニアは仕事があると私は胸を張って言えます。
これは「勤めている会社の中で仕事があるから」「自分がやっているから」というだけが理由ではありません。
私は趣味で英語の勉強会に行くのですが、その際職業の話になりアプリを作っていることを話すと、
結構な確率で「こういうサービスを考えているんだけど…」と相談されます。
今、何かサービスを作って流行らせようとする人は「アプリを作ろう」と考える人が多いと思います。
一昔前Webサイトのエンジニアが引っ張り凧だったように、アプリもサービスの入り口として考える人が多くなった今では、比較的仕事は取りやすくなっていると言えると思います。3. 将来性は?
いきなりマイナスな事を言いますが、近い将来アプリの言語が衰退するのは間違いないと思います。
その将来が何年後になるかは分かりませんが、スマホがもの凄い勢いで発展したように、
スマートグラスなのか、スマートウォッチなのかは分かりませんが、次のハードが進化した時、
必ずスマホ周りの言語も衰退します。
ただこれはどんな言語もほぼ同じなので、月並ですが、日々情報を追っていくしか無いと言えるでしょう。4. 学習コストは? Python、Ruby、C++と比べたら?
4-1) 難しさのレベルは?
私が学習したことのある言語で具体的にアプリ等を最後まで作ったことのある言語は、
Swift、Kotlin、Java、 JavaScript、GAS、Python、Ruby、C#(Unity)、C++です。
これらを実際に学習してみて、アプリのフロントで主に扱う言語のSwift、Kotlin、Java Script (React-Nativeなど)、C# (Unity)の学習コストを比較して考えてみると…
中くらいのレベルの難しさ、と僕は言いたいです。複数の言語を学習したことがある方ならお分かりいただけると思うのですが、
基本文法の学習コストはどれもそんなに変わりません。個々の好みの範囲と言えると思います。
現によくRubyやPythonは学習コストの低い言語として取り上げられますが、僕はあまりそう思いません。4-2) 学習コストの差
では学習コストで生まれる差は何か?
それは何に使うかです。Pythonなら機械学習など、RubyならWebアプリ作成など、C++ならPCのリソースに関わるプログラムの作成…などなどです。
ではアプリのフロントで使用する言語では?
当然スマホのアプリを書くためです。
他の言語と比較して目的がはっきりしている分、学習者が突っかかる問題も同じ事が多いです。
そのためネットでエラーを検索するとその解決法は他の言語に比べ見つけやすいです。
初学者にとって入門しやすいと私は思います。4-3) アプリのフロントならではの難しさ
ではなぜ「Swiftは簡単な言語」と、手放しでは言えないのか?
その主な理由はOSやハードウェアの進化にあります。
皆さんも日々生活されていて、新しいiPhoneのニュースなどはTVやYouTubeなどで、よく見かけると思います。
Androidも同様に日々革新的な進化をし続けています。
アプリのフロントはこれらを追い続けなければならないのです。
アプリのフロントの言語は初学者にとっては入門しやすいですが、どれだけ熟達しても学習が定期的に必要な言語と言えると思います。
これが私が簡単が言語とは言い難いと思う理由です。5.クロスプラットフォーム?ネイティブ?
少し突っ込んだ話になります。
まず学ぶなら、React-Nativeなどのクロスプラットフォームがいいか、ネイティブがいいか?
私はネイティブをオススメします。
というのもクロスプラットフォームを使ってもどのみちXcodeやAndroid Studioの使い方を知る必要がありうるからです。
例えばReact-Nativeでアプリを書いた場合、iPhoneの実機でのデバッグはXcodeからビルドする必要があります。
また、アプリのアーカイブやライブラリの設定、証明書の設定などをする際も同様です。
せっかくアプリを書いたのに、そこから使ったことのないツールをまた一から学習するのは少しうんざりしますよね。
ですので、チュートリアル程度でいいのでSwiftとKotlinである程度学習してから、クロスプラットフォームを使用することをオススメします。6.最後に
色々とごちゃごちゃ書きましたが、
私はアプリのフロントはとても楽しくやらしていただいております。
世の中から必要とされている実感がある、とてもやりがいのある仕事です。
この記事が少しでもスマホアプリのフロントエンドエンジニアになろうと思っている方のお役に立てたら、とても嬉しいです。
- 投稿日:2020-02-10T16:14:59+09:00
【Swift5】UIActivityIndicatorViewの実装,UITableView画面でぐるぐる回る~~
この文章のデモはこのようになる
本記事はUIActivityIndicatorViewの使い方を勉強する個人的メモとします.役に立てればうれしいです.
UIActivityIndicatorViewは下図のようなぐるぐる回るやつです,
Web経由のデータリクエストや重い処理でロードするときによく使います
実装方法
こんな感じで簡単に実装できます.
start ぐるぐる
処理を実行する
stop ぐるぐる本来であれば,Web経由のデータリクエストを行う際に,
データのレスポンスが来るまで画面の背景は何も表示しないのですが,
このデモはすでに存在するTableViewを隠すために”BackgroundView”を入れた.
ぐるぐる終了まで文字を隠すためです.(笑)Viewの構造は下図のようになる
そのViewの上に”TempView”を置き,色:black, 透明度:50%, 円角:6TempView.layer.cornerRadius = 6UIActivityIndicatorViewの実装1.
UIActivityIndicatorViewを用意し,アニメーションのスタート関数とストップ関数を作る.
@IBOutlet weak var TempView: UIView! // TempViewを置き @IBOutlet weak var backgroundview: UIView! // backgroundviewを置き @IBOutlet weak var activtiindicator: UIActivityIndicatorView! // StoryBoard上でUIActivityIndicatorViewを置き,変数名activtiindicatorとする @IBOutlet var TableViewPageTable: UITableView! // TbaleViewに変数名TableViewPageTableをつける // アニメーションを実行する関数を作る. func startAnimating() { activtiindicator.startAnimating() // アニメーションを実行する(ぐるぐる) activtiindicator.isHidden = false // アニメーションを実行する際,UIActivityIndicatorViewを表すようにする. } // アニメーションを停止する関数を作る. func stopAnimating() { if activtiindicator.isHidden == false { activtiindicator.stopAnimating() // アニメーションを停止する activtiindicator.isHidden = true // アニメーションの実行が終了後,UIActivityIndicatorViewを隠すようにする. } TempView.isHidden = true // アニメーションの実行が終了後,背景画面も一同に隠す. ViewBackground.isHidden = true // アニメーションの実行が終了後,背景画面も一同に隠す. }UIActivityIndicatorViewの実装2.
実際に実行し,重い作業を行っていることを想定する.
override func viewDidLoad() { super.viewDidLoad() TempView.layer.cornerRadius = 6 // TempViewの円角を6に設定. startAnimating() // ぐるぐる回りはじめる. DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: { // 2秒ぐらい待ち self.stopAnimating() // ぐるぐるが終了する. }) }~全体のコードは以下のURLでダウンロードできる.~
https://github.com/funsiyuan/UseActivityIndicator
- 投稿日:2020-02-10T01:39:46+09:00
ヒューマンエラーによる条件分岐漏れ防止に有効なSwiftにおける4つのTIPS
概要
Swiftに限らず、コードを書く中で外すことができない条件分岐。
プログラムを書く上で避けては通れない存在であるものの、実装を間違えればバグの温床ともなります。しかしSwiftにはこの条件分岐を正しく実装するのに役立つ仕組みが文法レベルで備わっています。
そこで自分が実際に現場で運用した実績のある、ヒューマンエラーを防ぐためのSwiftでの条件分岐の書き方を解説します。1. switch構文で極力defaultを使わない
概要
複数条件を完結に分岐せさせる事ができるswitch文。
大変便利ですし複雑な条件ではif
よりも使用をおすすめしたのですが、 1つだけ気をつけなければならないのはdefault
の使用ですご存知の通り
default
はどのcase
にも合致しない場合に呼ばれる条件文です。
しかし 一度各caseを定義した後に新たに追加したcaseについては、defaultを使っていると分岐の対応漏れを発生させるリスクとなります。具体例
あるサービスで、利用状況に応じてユーザーにランクをつけるような機能があったとします。
そしてサービスの設計時には、仕様として「GOLD」「SILVER」の2種類のみの会員が存在したとし、 GOLD 会員に対しては割引価格を提供するロジックが存在したとします。
この時、会員ランクと会員の情報は以下のように設計されていました。/// 会員ランク enum UserRank { case .gold case .silver } /// ユーザー struct User { let userRank: UserRank ... /// 会員ランクごとに割引または割増調整された価格を返す /// /// - Parameters: /// - defaultPrice: 通常価格 /// - Returns: 調整された価格 func fetchSpecialPrice(basedOn defaultPrice: Double) -> Double { switch self.userRank { case .gold: // GOLD会員は5%割引 return defaultPrice * 0.95 default: // SILVER会員は通常価格 return defaultPrice } } }しかしサービスを運用する中で「GOLDの中でも特に優良なユーザーさんをプラチナ(PLATINUM)会員と認定し、GOLDよりもさらに割引した特別価格(10%引き)を提供しよう 」という要件が加わったとします。
そこで上記のうちUserRank
を次のように変更しました。enum UserRank { case .platinum // 新規追加 case .gold case .silver }これだけでビルドしてコンパイルを通すと、エラーや警告は特に発生しないため、このままリリースもできていしまいます。
ところが「GOLDよりもさらに割引した特別価格を提供」するロジックが実装漏れしています。
つまり 本来であれば下記の修正も必要になるのです。func fetchSpecialPrice(basedOn defaultPrice: Double) -> Double { switch self.userRank { case .platinum: // [NEW] PLATINUM会員は10%割引 return defaultPrice * 0.90 case .gold: // GOLD会員は5%割引 return defaultPrice * 0.95 default: // SILVER会員は通常価格 return defaultPrice } } }上記対応がなくても コンパイル時点では文法的に問題がないため、対応漏れに気づくことができませんでした
これは 新規に定義されたcase
の条件分岐をdefault
が暗黙的にハンドリングしてしまったことに起因します。つまり、
default
を使わず全case
を明示的に指定して分岐していたならば、新しいcase
が定義された時点で以下のようなエラーが発生して気づけていたはずですSwitch must be exhaustive,consider adding a default clause.これは「全caseを網羅しきれていないから、(足りていないcaseの分岐を作るか)default文を使え」というメッセージです。
エラーが文法チェックの段階で発生し、条件分岐の実装漏れにコンパイルで気づくことができるわけです。
上記が1箇所であればまだしも、コードのあちこちに同様の分岐が散らばっている場合には漏れの被害も相当になりますよね。このように
default
は便利ではありながらも、暗黙的に足りないcaseをハンドリングしてしまうため、本当に必要な分岐の実装漏れに気づきにくいリスクを発生させます。そのため、個人的には
default
の使用を可能な限り(Int
やString
では無理なので)控えるようチームに説明しています。補足
なおSwift 5では
@unknown
属性という新しいアノテーションが定義されました。
これは新しいcaseが増えた時にdefault
の使用箇所があれば漏れている可能性があると警告するというものです。
しかしこれは、 あくまでも「警告」なので、当然ですが抑制したり無視したりすることはできてしまいます。
そのため、個人的にはこの機能を使うことはあっても、上記の運用をやめることはまずないだろうと想定しています。2. 多値分岐が発生する余地が0%ではない限りenumの使用を検討する
概要
真偽値を表す
Bool
型も、ほぼ全ての言語で使われていてどんな方にも馴染みがあるのでないかと思います。
真偽値はtrue
とfalse
の2値を排他的に表すための型ですが、 もしそれ以外の値=多値での分岐が発生する余地があるならenum
が適切です。
またSwiftのenumには、特定のcase
にのみ必要な情報を渡すための仕組み(associated value)があるので、そういった制約があったとしてもenumの使用を検討すべきだと思います。具体例
例えば国内と海外に展開することを想定して作られたあるサービスがあったとします。
その場合、下記のようにサービス内での条件分岐をBool
で行うことは容易です。/// ユーザーを表す struct User { /// 国内ならtrue 国外ならfalse let isDomestic: Bool ... } let user: User = .init(isDomestic: ...) if user.isDomestic { // 国内ユーザー向けロジック } else { // 国外ユーザー向けロジック }しかしサービス開始時点で、将来の対応先が「海外」が未来永劫1カ国あるいは各国共通であることは、大抵の場合保証されていません。
事業が成功すれば複数カ国に展開するかもしれませんし、そうなれば言語や内部ロジックも国ごとに最適化する必要が出てくる可能性があります。その場合
Bool
では国内か国外かの2値でしか判別できず、海外の複数カ国内でのロジック分岐には別途国判別のロジックが必要になりますし、場合によってはサーバーサイドロジックとの整合性が壊れてきます。
もし上記の実装を維持したまま新たに国判定の実装をする場合、下記のようなコードが追加されることになります。struct User { /// 国内ならtrue 国外ならfalse let isDomestic: Bool /// [NEW] 国外向けに国を判定するための種別 let country: Country ... } /// [NEW] 国の種別 enum Country { case .us // 米国 case .ch // 中国 } let user: User = .init(isDomestic: ..., country: Country) if user.isDomestic { // 国内ユーザー向けロジック } else { // [NEW] switch country { case .us: // 米国ユーザー向けロジック case .ch: // 中国ユーザー向けロジック ... } }これでは
isDomestic
とcountry
が表すドメインが被ってしまいますし、後からメンテナンスをしていくときに分岐漏れなどが発生する原因になり負債になりかねません。
もし設計の段階で 2値分岐ではなくなる余地が0.1%でもあるなら、個人的には多値で分岐可能かつロジックを集約可能なenumで定義するのがベストプラクティスだと思います。たとえば初めから国を指定するのではなく、先程の
isDomestic
のようなフラグをassociated value enumで定義するものありです。
こうすることで「国外」という情報とセットで「国」の情報も渡すことができますし、switch
が使えるので前述のdefault
の不使用と合わせて条件分岐漏れをなくすことができます。/// サービス展開地域 enum Region { /// 国内 case domestic /// 国外 /// /// - Parameters: /// - country: 国 case foreign(country: ForeignCountry) /// 国外向けの国種別 enum ForeignCountry: String { case us case ch } } struct User { let region: Region } let user: User = .init(region: .foreign(country: .us)) switch user.region { case .domestic: // 国内向けロジック break case .foreign(let country): // 国外向け共通ロジック switch country { case .us: // 米国向けロジック break case .ch: // 中国向け共通ロジック break } }このような「2値と思われがちだが要件次第で多値になりうる」属性の例としては、他にも下記のようなものがあります。
- 性別 (LGBT等)
- 有料無料 (有料内に前述で示した「ランク」のような概念が発生する可能性)
- 禁煙喫煙 (分煙や「iQOSはOK」等の要件が発生する可能性)
enumの定義は増えますが、各属性値を明確に表すにはぜひこの方法を活用していきたいものです。
3. 条件分岐の直前で定数・変数を定義し分岐内で代入する
概要
こちらは自分が最近まで知らなかったこと。
前述のswitchでの分岐の話に近いですが、多値分岐のcase内で変数や定数を初期化したいときは、 条件分岐前に変数や定数の宣言だけをすることができます。
そして各分岐内で初期化をし、もしできていない分岐がある場合には初期化されないことを表す文法エラーが発生します。error: constant 'x' used before being initializedしかし 宣言時に初期化も行っている場合は、上記のようなエラーが発生しないので実装漏れが発生するリスクとなります。
具体例
天候条件
WeatherCondition
によって、それぞれにあった持ち物Belonging
を生成するロジックがあったとします。/// 条件分岐の対象 enum WeatherCondition: String { case sunny case cloudy case rainy } /// 持ち物 struct Belonging { /// 名前 var name: String /// 個数 var count: Int } /// 天候条件に合わせた持ち物を返す /// /// - Parameters: /// - condition: 天候条件 /// - Returns: 持ち物 func fetchBelonging(for condition: WeatherCondition) -> Belonging { // ここで初期化するとcaseが書き漏れてもコンパイルエラーにはならなず気づかない var name: String = "", count: Int = 0 switch condition { case .sunny: name = "lunch box" count = 2 case .cloudy: // 初期化されていないがエラーにはならない print("Something") case .rainy: name = "umbrella" count = 1 } return .init(name: name, count: count) } fetchBelonging(for: .cloudy)すると上記では前述の
default
文と同様に 「初期値 = デフォルト値が設定されている」という理由で条件分岐漏れに気づけません。
この場合は、デフォルト値を入れた変数ではなく、空の定数を定義した以下の実装にするのが良いと考えています。func fetchBelonging(for condition: WeatherCondition) -> Belonging { // nameとcountは必ずどのcaseでも初期化される必要がある let name: String, count: Int switch condition { case .sunny: name = "lunch box" count = 2 case .cloudy: // 初期化されていないのでエラーになる print("Something") case .rainy: name = "umbrella" count = 1 } return .init(name: name, count: count) } fetchBelonging(for: .cloudy)これをコンパイルするとエラーにより失敗し、ロジックの分岐漏れに気づくことができます。
error: constant 'name' used before being initialized error: constant 'name' used before being initialized自分はそもそも
switch
の手前でlet
やvar
を初期化せず宣言のみできることを最近まで知りませんでした
今までわざわざjQueryのような即時実行関数を作って返していましたが必要なかったんですね// 下記のような即時関数を用いた実装は必要なかった let x: Int = { (arg: SomeType) -> Int in switch arg { case .some: return 1 ... } }4. enumには明示的にrawValueを指定する
概要
rawValueを使って分岐を行っている場合の話ですが、そういった場合はcase名と実際の値を疎結合にしておく方が予期せぬ変更の影響を受けにくいです。
例えばString
やInt
を継承したenumであれば、冗長と感じても最初にcase名と同じStringやIntを明示的に代入することをおすすめします。なぜならcase名がリファクタリング等で変わったとしてもコード的に
rawValue
は同じであることを求められるからです。
もしcase名のリファクタリング等によってrawValue
まで変わってしまうならリファクタリングしにくいコードになってしまいます。具体例
APIにリクエストを投げるためのメソッド
callSearchAPI
があり、その引数にはパラメータをenumで渡せるとします。/// APIをコールする /// /// - Parameters: /// - param: GETパラメータの種別 /// - value: GETパラメータの値 func callSearchAPI(with param: Parameter, and value: String) { let url: "https://api.example.com?\(param.rawValue)=\(value)" // rawValueをクエリパラメータとして仕様 request(url) } /// APIコール時に送信するクエリパラメータの種別 enum Parameter: String { case query case gender } callSearchAPI(with Parameter.query, and "my search query")(説明のためにあえてAssociated Value Enumは使っていません)
この場合では
Parameter.query.rawValue
は "query" に、Parameter.gender.rawValue
は "gender" になります。
しかしその後アプリ内だけでパラメータ名をリファクタリングしたいと思ったとします。
例えばgender
をgenderType
とリネームしたい場合には、下記のように定義だけを変更すると送られる値 = rawValue まで変わってしまいます。enum Parameter: String { case query case genderType // Parameter.genderType.rawValueは"genderType"になってしまう }するとサーバーサイドは依然GETパラメータ名を "gender" で受けているのでリクエストが弾かれてしまいます。
このようにデフォルトでcase名をrawValueにしてくれる機能は大変ありがたいのですが、逆に変数名と値が密結合してしまうことにもなります。
そこで自明ではあっても予めリスト3.2のようにrawValueは定義しておくのが良いです。enum Parameter: String { case query = "query" // <- case名を変更してもrawValueは "query" のまま case genderType = "gender" // <- case名を変更してもrawValueは "gender" のまま }こうすることで変数名の変更に値が引っ張られて意図しない値に変わることはありません。
まとめ
以上個人的に条件分岐漏れを防ぐためにやっているTIPSを紹介しました。
参考になれば幸いです。参考
- 投稿日:2020-02-10T01:39:46+09:00
Swiftで条件分岐漏れを防ぐのに有効な4つの書き方TIPS
概要
Swiftに限らず、コードを書く中で外すことができない条件分岐。
プログラムを書く上で避けては通れない存在であるものの、実装を間違えればバグの温床ともなります。しかしSwiftにはこの条件分岐を正しく実装するのに役立つ仕組みが文法レベルで備わっています。
そこで自分が実際に現場で運用した実績のある、ヒューマンエラーを防ぐためのSwiftでの条件分岐の書き方を解説します。1. switch構文で極力defaultを使わない
概要
複数条件を完結に分岐せさせる事ができるswitch文。
大変便利ですし複雑な条件ではif
よりも使用をおすすめしたのですが、 1つだけ気をつけなければならないのはdefault
の使用ですご存知の通り
default
はどのcase
にも合致しない場合に呼ばれる条件文です。
しかし 一度各caseを定義した後に新たに追加したcaseについては、defaultを使っていると分岐の対応漏れを発生させるリスクとなります。具体例
あるサービスで、利用状況に応じてユーザーにランクをつけるような機能があったとします。
そしてサービスの設計時には、仕様として「GOLD」「SILVER」の2種類のみの会員が存在したとし、 GOLD 会員に対しては割引価格を提供するロジックが存在したとします。
この時、会員ランクと会員の情報は以下のように設計されていました。会員ランクと値引き価格を算出するコード/// 会員ランク enum UserRank { case .gold case .silver } /// ユーザー struct User { let userRank: UserRank ... /// 会員ランクごとに割引または割増調整された価格を返す /// /// - Parameters: /// - defaultPrice: 通常価格 /// - Returns: 調整された価格 func fetchSpecialPrice(basedOn defaultPrice: Double) -> Double { switch self.userRank { case .gold: // GOLD会員は5%割引 return defaultPrice * 0.95 default: // SILVER会員は通常価格 return defaultPrice } } }しかしサービスを運用する中で「GOLDの中でも特に優良なユーザーさんをプラチナ(PLATINUM)会員と認定し、GOLDよりもさらに割引した特別価格(10%引き)を提供しよう 」という要件が加わったとします。
そこで上記のうちUserRank
を次のように変更しました。platinumを新規に追加enum UserRank { case .platinum // 新規追加 case .gold case .silver }これだけでビルドしてコンパイルを通すと、エラーや警告は特に発生しないため、このままリリースもできていしまいます。
ところが「GOLDよりもさらに割引した特別価格を提供」するロジックが実装漏れしています。
つまり 本来であれば下記の修正も必要だったのですが気づけなかったのです。platinumのcaseを新たに追加する必要があったfunc fetchSpecialPrice(basedOn defaultPrice: Double) -> Double { switch self.userRank { case .platinum: // [NEW] PLATINUM会員は10%割引 return defaultPrice * 0.90 case .gold: // GOLD会員は5%割引 return defaultPrice * 0.95 default: // SILVER会員は通常価格 return defaultPrice } } }上記対応がなくても コンパイル時点では文法的に問題がないため、対応漏れに気づくことができませんでした
これは 新規に定義されたcase
の条件分岐をdefault
が暗黙的にハンドリングしてしまったことに起因します。つまり、
default
を使わず全case
を明示的に指定して分岐していたならば、新しいcase
が定義された時点で以下のようなエラーが発生して気づけていたはずですdefaultを使わなければ新しいcaseが出た時にコンパイルエラーになるfunc fetchSpecialPrice(basedOn defaultPrice: Double) -> Double { switch self.userRank { case .gold: // GOLD会員は5%割引 return defaultPrice * 0.95 case .silver: // SILVER会員は通常価格 return defaultPrice } } }Switch must be exhaustive,consider adding a default clause.これは「全caseを網羅しきれていないから、(足りていないcaseの分岐を作るか)default文を使え」というメッセージです。
エラーが文法チェックの段階で発生し、条件分岐の実装漏れにコンパイルで気づくことができるわけです
上記が1箇所であればまだしも、コードのあちこちに同様の分岐が散らばっている場合には漏れの被害も相当になりますよね。このように
default
は便利ではありながらも、暗黙的に足りないcaseをハンドリングしてしまうため、本当に必要な分岐の実装漏れに気づきにくいリスクを発生させます。そのため、個人的には
default
の使用を可能な限り(Int
やString
では無理なので)控えるようチームに説明しています。補足
なおSwift 5では
@unknown
属性という新しいアノテーションが定義されました。
これは新しいcaseが増えた時にdefault
の使用箇所があれば漏れている可能性があると警告するというものです。
しかしこれは、 あくまでも「警告」なので、当然ですが抑制したり無視したりすることはできてしまいます。
そのため、個人的にはこの機能を使うことはあっても、上記の運用をやめることはまずないだろうと想定しています。2. 多値分岐が発生する余地が0%ではない限りenumの使用を検討する
概要
真偽値を表す
Bool
型も、ほぼ全ての言語で使われていてどんな方にも馴染みがあるのでないかと思います。
真偽値はtrue
とfalse
の2値を排他的に表すための型ですが、 もしそれ以外の値=多値での分岐が発生する余地があるならenum
が適切です。
またSwiftのenumには、特定のcase
にのみ必要な情報を渡すための仕組み(associated value)があるので、そういった制約があったとしてもenumの使用を検討すべきだと思います。具体例
例えば国内と海外に展開することを想定して作られたあるサービスがあったとします。
その場合、下記のようにサービス内での条件分岐をBool
で行うことは容易です。ユーザー定義と国内・国外を判別するコード/// ユーザーを表す struct User { /// 国内ならtrue 国外ならfalse let isDomestic: Bool ... } let user: User = .init(isDomestic: ...) if user.isDomestic { // 国内ユーザー向けロジック } else { // 国外ユーザー向けロジック }しかしサービス開始時点で、将来の対応先が「海外」が未来永劫1カ国あるいは各国共通であることは、大抵の場合保証されていません。
事業が成功すれば複数カ国に展開するかもしれませんし、そうなれば言語や内部ロジックも国ごとに最適化する必要が出てくる可能性があります。その場合
Bool
では国内か国外かの2値でしか判別できず、海外の複数カ国内でのロジック分岐には別途国判別のロジックが必要になりますし、場合によってはサーバーサイドロジックとの整合性が壊れてきます
もし上記の実装を維持したまま新たに国判定の実装をする場合、下記のようなコードが追加されることになります。国外で新たに国を判別することになった場合のコードstruct User { /// 国内ならtrue 国外ならfalse let isDomestic: Bool /// [NEW] 国外向けに国を判定するための種別 let country: Country ... } /// [NEW] 国の種別 enum Country { case .us // 米国 case .ch // 中国 } let user: User = .init(isDomestic: ..., country: Country) if user.isDomestic { // 国内ユーザー向けロジック } else { // [NEW] switch country { case .us: // 米国ユーザー向けロジック case .ch: // 中国ユーザー向けロジック ... } }これでは
isDomestic
とcountry
が表すドメインが被ってしまいますし、後からメンテナンスをしていくときに分岐漏れなどが発生する原因になり負債になりかねません。
もし設計の段階で 2値分岐ではなくなる余地が0.1%でもあるなら、個人的には多値で分岐可能かつロジックを集約可能なenumで定義するのがベストプラクティスだと思います。たとえば初めから国を指定するのではなく、先程の
isDomestic
のようなフラグをassociated value enumで定義するものありです。
こうすることで「国外」という情報とセットで「国」の情報も渡すことができますし、switch
が使えるので前述のdefault
の不使用と合わせて条件分岐漏れをなくすことができます。国外に別途associated-value-enumで国情報を渡す/// サービス展開地域 enum Region { /// 国内 case domestic /// 国外 /// /// - Parameters: /// - country: 国 case foreign(country: ForeignCountry) /// 国外向けの国種別 enum ForeignCountry: String { case us case ch } } struct User { let region: Region } let user: User = .init(region: .foreign(country: .us)) switch user.region { case .domestic: // 国内向けロジック break case .foreign(let country): // 国外向け共通ロジック switch country { case .us: // 米国向けロジック break case .ch: // 中国向け共通ロジック break } }このような「2値と思われがちだが要件次第で多値になりうる」属性の例としては、他にも下記のようなものがあります。
- 性別 (LGBT等)
- 有料無料 (有料内に前述で示した「ランク」のような概念が発生する可能性)
- 禁煙喫煙 (分煙や「iQOSはOK」等の要件が発生する可能性)
enumの定義は増えますが、各属性値を明確に表すにはぜひこの方法を活用していきたいものです。
3. 条件分岐の直前で定数・変数を定義し分岐内で代入する
概要
こちらは自分が最近まで知らなかったこと。
前述のswitchでの分岐の話に近いですが、多値分岐のcase内で変数や定数を初期化したいときは、 条件分岐前に変数や定数の宣言だけをすることができます。
そして各分岐内で初期化をし、もしできていない分岐がある場合には初期化されないことを表す文法エラーが発生します。error: constant 'x' used before being initializedしかし 宣言時に初期化も行っている場合は、上記のようなエラーが発生しないので実装漏れが発生するリスクとなります。
具体例
天候条件
WeatherCondition
によって、それぞれにあった持ち物Belonging
を生成するロジックがあったとしますcloudyでの初期化を忘れたことに気づけないコード/// 条件分岐の対象 enum WeatherCondition: String { case sunny case cloudy case rainy } /// 持ち物 struct Belonging { /// 名前 var name: String /// 個数 var count: Int } /// 天候条件に合わせた持ち物を返す /// /// - Parameters: /// - condition: 天候条件 /// - Returns: 持ち物 func fetchBelonging(for condition: WeatherCondition) -> Belonging { // ここで初期化するとcaseが書き漏れてもコンパイルエラーにはならなず気づかない var name: String = "", count: Int = 0 switch condition { case .sunny: name = "lunch box" count = 2 case .cloudy: // 初期化されていないがエラーにはならない print("Something") case .rainy: name = "umbrella" count = 1 } return .init(name: name, count: count) } fetchBelonging(for: .cloudy)(説明のためにあえて冗長な実装にしています)
すると上記では前述の
default
文と同様に 「初期値 = デフォルト値が設定されている」という理由で条件分岐漏れに気づけません。
この場合は、デフォルト値を入れた変数ではなく、空の定数を定義した以下の実装にするのが良いと考えています。cloudyでの初期化を忘れたことに気づけるコードfunc fetchBelonging(for condition: WeatherCondition) -> Belonging { // nameとcountは必ずどのcaseでも初期化される必要がある let name: String, count: Int switch condition { case .sunny: name = "lunch box" count = 2 case .cloudy: // 初期化されていないのでエラーになる print("Something") case .rainy: name = "umbrella" count = 1 } return .init(name: name, count: count) } fetchBelonging(for: .cloudy)これをコンパイルするとエラーにより失敗し、ロジックの分岐漏れに気づくことができます。
error: constant 'name' used before being initialized error: constant 'count' used before being initialized自分はそもそも
switch
の手前でlet
やvar
を初期化せず宣言のみできることを最近まで知りませんでした
今までわざわざjQueryのような即時実行関数を作って返していましたが必要なかったんですね即時関数を用いた実装は必要なかったlet x: Int = { (arg: SomeType) -> Int in switch arg { case .some: return 1 ... } }4. enumには明示的にrawValueを指定する
概要
enumでrawValueを参照する実装がある場合、 case名と実際の値を疎結合にしておく方が予期せぬ変更の影響を受けにくいです。
例えばString
やInt
を継承したenumであれば、冗長と感じても最初にcase名と同じStringやIntを明示的に代入することをおすすめします。
なぜなら case名がリファクタリング等で変わったとしてもコード的にrawValue
はリファクタリング前と同値な必要があるからです。
もしcase名のリネーム等をした時にrawValue
まで変わってしまうと リファクタリングがしにくいコードになってしまいます。具体例
APIにリクエストを投げるためのメソッド
callSearchAPI
があり、その引数にはパラメータをenumで渡せるとします。case名とrawValueが密結合しているコード/// APIをコールする /// /// - Parameters: /// - param: GETパラメータの種別 /// - value: GETパラメータの値 func callSearchAPI(with param: Parameter, and value: String) { let url: "https://api.example.com?\(param.rawValue)=\(value)" // rawValueをクエリパラメータとして仕様 request(url) } /// APIコール時に送信するクエリパラメータの種別 /// case名 = rawValueになっている enum Parameter: String { case query case gender } callSearchAPI(with Parameter.query, and "my search query")(説明のためにあえてAssociated Value Enumは使っていません)
上記の例では
Parameter.query.rawValue
は "query" に、Parameter.gender.rawValue
は "gender" になります。しかし実装後に アプリ内だけでパラメータ名をリネームしたいと思ったとします。
例えばgender
をgenderType
とリネームする時に、下記のように定義だけを変更すると送られる値 = rawValue まで変わってしまいます。case名を変えた結果rawValueまで変わってしまうコードenum Parameter: String { case query // Parameter.genderType.rawValueは"genderType"になってしまう // サーバーサイドは依然として"gender"を要求するのでリクエストが失敗してしまう case genderType }すると サーバーサイドは依然GETパラメータ名を "gender" で受けているのでリクエストが弾かれてしまいます
このようにデフォルトでcase名をrawValueにしてくれる機能は明示的な宣言が不要で便利なのですが、 逆に変数名と値が密結合してしまう原因にもなります。
そこで、 自明ではあっても次のようにrawValueはきちんと定義しておくのが良いと思うのです。case名の変更に強いコードenum Parameter: String { case query = "query" // <- case名を変更してもrawValueは "query" のまま case genderType = "gender" // <- case名を変更してもrawValueは "gender" のまま }こうすることで変数名の変更に値が引っ張られて意図しない値に変わることはありません
まとめ
以上個人的に条件分岐漏れを防ぐために日頃やっているTIPSを紹介しました。
条件分岐漏れはどんな現場でも毎日のように発生しているバグですし、自分もこれまで散々苦しんできました。しかし幸いにもSwift文法は簡潔で良い設計がしやすいので、ぜひその特性を活かしてコードを書いていくことでハッピーになれるのではと思った次第です。
以上が少しでも皆さんの参考になれば幸いです。参考