Swiftを学んでみたのでPaizaのサンプルコードを直してみた

技術,プログラミング言語

最近、モダンな言語の学習シリーズとしてSwiftを学んでいます。で、PaizaのスキルチェックもSwiftに対応しているので、それをお題にしてみようかといつものサンプル入出力コードを見たのですが…あまりSwiftっぽくないような。

というわけで、手を入れてみました。

素の状態

オリジナルのサンプルコードは以下の通りです。入力として標準入力から入力する行数を入力し、その分だけスペース区切りで2つの文字列を「XXX YYY」のように入力します。すると、「hello = XXX, world = YYY」と出力するだけの簡単なサンプルです。

import Foundation

let n = Int(readLine()!)!
for i in 1...n {
    let s = readLine()!.components(separatedBy: " ")
    print("hello = " + s[0] + " , world = " + s[1])
}

手直し1:警告をなくす

まあ、動くのですが、実行すると警告が出ます。

paiza-sample.swift:4:5: warning: immutable value 'i' was never used; consider replacing with '_' or removing it
for i in 1...n {
    ^
    _

それ、サンプルとしてどうなんだろう?と自分は思うので、警告が出ないようにします。

警告内容を超訳すると「"i"とかいう変数を定義してるけど使ってねーじゃん。そういうのは"_"を使っとけ!」になるので、そのようにします。

import Foundation

let n = Int(readLine()!)!
for _ in 1...n {
    let s = readLine()!.components(separatedBy: " ")
    print("hello = " + s[0] + " , world = " + s[1])
}

これでめでたく警告が出なくなります。

“_"ってなに?

関数の返り値を使わない時にこれは使わないですよという意味で「_ = foo()」と記述します。ただ、あまり明確に説明している部分が公式ドキュメントの中から見つけられませんでした。なんでだろう?

手直し2:入力1行目(行数)のエラー対策

Paizaのスキルチェックは競技プログラミング(競プロ)を意識していると感じますが、競プロのコードってエラーチェックしないのですよね。それをプログラミングと言ってよいのかが非常に微妙に感じるので、自分は競プロはプログラミング風パズルだと思っています。

それはさておき、競プロ的には気にしないのでしょうが、個人的に気になるので行数入力部分のエラー処理をしてみます。ついでに、変数名nも気に入らないので"numInputLines"に変えました。本当は終了時に適切なメッセージを表示すべきですが、そこまではしないことにしました。ついでに言えば、exitの引数である終了コードもエラー内容に応じて変えるべきです。

import Foundation

// 1行入力(空なら終了)
guard let line = readLine() else {
    exit(-1)
}
// 入力された行を数値に変換(変換できなければ終了)
guard let numInputLines = Int(line.trimmingCharacters(in: .whitespaces)) else {
    exit(-1)
}
// 変換した数値が0以下ならば終了
guard numInputLines > 0  else {
    exit(-1)
}

for _ in 1...numInputLines {
    let s = readLine()!.components(separatedBy: " ")
    print("hello = " + s[0] + " , world = " + s[1])
}

nilとOptional

Swiftを語る上で避けて通れないのがnilとOptionalです。

nilはCならNULL、C++とかJavaならnullのことです。未定義状態を指します。そして、Swiftではデフォルトでnilを許容しません。nilは未定義なので、それを利用しようとするとアプリケーションが異常終了します。Javaでは"NullPointerException"という実行時エラーが出るので、よく「ぬるぽ」とか呼びます(もちろん、日本特有の表現)。

安全を重視しているSwiftではあえてnilを許容するという記述をしていない部分ではnilを許さないことで安全性を確保しています。とは言え、nilがないと困るので、許容する部分ではOptionalというものを使います。?とか!とかついている部分が概ねnil許容とOptional関連になります。

アンラップとguard文

nilを許容しているとは言え、それをそのままでは処理できません。Optionalはnilがあるかも知れないよくわからないものをラップした状態であるという考え方になっており、nilでないことを確認した上で中身を取り出す必要があります。それをアンラップと呼びます。

アンラップの仕方にはいくつかの方法がありますが、ここでは4行目から6行目の様にguard文を使っています。guardは何かをチェックし、結果がNGだった場合に処理を中断するような場合に使うものです。この場合、readLine()で1行読み取りますが、その結果がnilだった場合にexit(-1)でプログラムを終了します。

8から10行目も同様で、読み取った1行をトリムした上で、Int()で数値に変換していますが、これも数値に変換できない文字が入っていたりするとnilになるので、アンラップしています。なお、言語によっては数値と取れればいい感じに変換してくれる言語もありますが、Swiftは厳密に変換します。数字以外の文字が入っていれば軒並みnilになります。それが空白でもnilなのでトリミングしています。"123a"を123として変換してくれたりしないし、"1 2″もnilです。

12から14行目はguard文を使っていますが、ここはアンラップとは関係ありません。0とか負の数とか、数値に変換できたけど入力行数としてはおかしいものだった場合に、中断する処理を入れています。以下のようなif文でも書けますが、guard文の方がわかりやすいです。

if numInputLines > 0 {
   // 処理したい内容
}

考え方としてはrubyとかにあるunlessに近いですが、guardは条件が偽であった場合、処理を中断したり、ループを抜けたりするのがお約束であって、処理を続けてはいけないことになっています。そのため、プログラマはコードを読んでいてguard文を見つけたら、条件を満たさない場合は処理が終わるのだと理解すればよいことになります。

例えば、以下のようなコードを書くとエラーになります。

let a = 0
guard a != 0 else {
    print("NG")
}
% swift guard-sample.swift 
guard-sample.swift:4:1: error: 'guard' body must not fall through, consider using a 'return' or 'throw' to exit the scope
}
^

ぬるぽで苦労したことない人から見ると、何やら分かりにくくて冗長に感じるかも知れませんが、ぬるぽで苦労したことがあれば、なんて素晴らしい!と思うことでしょう。

強制アンラップ

元のサンプルにあった「let n = Int(readLine()!)!」というやたらと!がついている1行が強制アンラップと呼ばれるものです。nilがあるかも知れないけど強制的に中身を取り出してしまう処理です。ですので、nilだった場合はぬるぽになります。ぬるぽになるとびっくりするから!をつけるのかどうかは知りません。

基本書き捨ての競プロでは平気でこういうぬるぽが起きうるコードを書きます。まあ、使い捨てするコードであれば構わないので、こういう強制アンラップという危ないコードも書けるようになってはいます。とは言え、やはりちゃんとアンラップすべきだと思いますので、わざわざ手間をかけています。

ただ、競プロではコードを書く速さや実行速度も重要であり、1行で済むことをコメント抜きで6行もかけてするのは不利になります。だからと言って、iPhoneやMacのアプリでこんなコードを書くのはいかがなものかと。プログラミングもTPOが重要です。

エラーハンドリング

エラーハンドリング(例外処理)で済ます手もありそうですが、これはうまくいきませんでした。Int()が例外を投げてくれないので、拾えないようです。

var n = 0
do {
    try n = Int(readLine()!)!
} catch {
    print("error!")
}
print(n)

Swiftの場合、関数やメソッドが例外をthrowする場合、呼び出し元は必ずそれをハンドリングしないといけないため、何でもかんでも例外をthrowすると呼び出し元が大変になります。そのため、あえて例外を投げていないのかと思います。

手直し3:2行目以降もエラー処理する

さて、1行目の行数読み取り部分のエラー処理をしたので、引き続き2行目以降の入力を受け取るループ部分のエラー処理です。まあ、やることは同じです。ただ、今はループの中身は2行ですが、ここも行数が増えるので、関数に分けたいと思います。何らかの問題が起きたときはfalseを返し、問題なければtrueを返すものとします。また、1文字変数名のsもitemsに変えました。競プロではわかればよいのでしょうが、プログラミングではわかる名前にするべきだと思います。

import Foundation

// 処理のメイン部分
func procEachLines() -> Bool {
    // 1行読み込み
    guard let line = readLine() else {
        return false
    }
    // 空白で分ける(トリミングする)
    let items = line.trimmingCharacters(in: .whitespaces).components(separatedBy: " ")
    // 分割した項目が2つ以上あるかチェック
    guard items.count > 1 else {
        return false
    }
    // 出力
    print("hello = \(items[0]), world = \(items[1])")
    return true
}

// 1行入力(空なら終了)
guard let line = readLine() else {
    exit(-1)
}
// 入力された行を数値に変換(変換できなければ終了)
guard let numInputLines = Int(line.trimmingCharacters(in: .whitespaces)) else {
    exit(-1)
}
// 変換した数値が0以下ならば終了
guard numInputLines > 0  else {
    exit(-1)
}

for _ in 1...numInputLines {
    // 1行読み込んで出力する
    if procEachLines() == false {
        exit(-1)
    }
}

だいぶ長くなってきました。

各行の処理部分

4から19行目までの関数に分けました。7から9行目は1行読む部分ですが、ここは先ほどの1行読む部分と変わりません。lineという同じ変数名を関数内でも使っていますが、スコープが異なるので問題ありません。同じ用途の変数名は統一しておいた方が良いかと思います。

11行目ではなくても良いのですが、読み込んだ1行の前後の空白文字をトリミングしています。競プロでは絶対にしないでしょう。その上でスペース区切りで分割しています。他の言語ではtrimやsplitといった名前ですが、Swiftは名前が長いですね。

13から15行目で分割した項目が2つ以上あるかチェックしています。これをしないとスペース区切りを忘れると異常終了してしまいます。

最後に17行目で文字列を組み立てて出力しています。オリジナルでは文字列を連結していましたが、今っぽくないので書き換えています。ただ、バックスラッシュは日本語キーボードでは入力しにくいのが難点です。他の言語のように「${…}」とか「#{…}」みたいな形式の方がありがたかったような気はします。

整えます

これでも動きますが、各行処理の部分を関数に分けたので、バランスが良くないと感じます。ですので、行数を取得する部分も関数に分けます。本当はメイン関数も作りたいところですが、Swiftにはないので、そこは我慢。メイン部分を頭に持ってきます。

行数取得関数では問題が起きたら0を、問題なければ入力された行数を返すことにします。

import Foundation

// 行数を取得(取得した数値が0以下ならば終了)
let numInputLines = readInputLines()
guard numInputLines > 0  else {
    exit(-1)
}

for _ in 1...numInputLines {
    // 1行読み込んで出力する
    if procEachLines() == false {
        exit(-1)
    }
}

// 1行入力(空なら終了)///////////////////////////////////
func readInputLines() -> Int {
    guard let line = readLine() else {
        return 0
    }
    // 入力された行を数値に変換(変換できなければ終了)
    guard let num = Int(line.trimmingCharacters(in: .whitespaces)) else {
        return 0
    }
    return num
}

// 処理のメイン部分 //////////////////////////////////////
func procEachLines() -> Bool {
    // 1行読み込み
    guard let line = readLine() else {
        return false
    }
    // 空白で分ける(トリミングする)
    let items = line.trimmingCharacters(in: .whitespaces).components(separatedBy: " ")
    // 分割した項目が2つ以上あるかチェック
    guard items.count > 1 else {
        return false
    }
    // 出力
    print("hello = \(items[0]), world = \(items[1])")
    return true
}

まとめ

数行のプログラムが40行を超えてしまいました。競プロでこんなコードを書いたら減点ものかも知れませんが、実用的なプログラムではエラー処理は必須ですし、そういう練習を積んでおくことは良いことです。当たり前のロジックを当たり前にかけるのは必要なことではありますが、当たり前のことです。プログラミングを職とするのであれば、こういう当たり前以外の部分も当たり前に書けないといけないです。

と偉そうなことを書きましたが、Swiftについてはまだまだ初心者の域を出ませんので、もしかするとSwiftっぽくなかったりする部分があるかも知れません。何かあればコメントにお願いします。

最後に、エラーハンドリングの練習を兼ねてエラーハンドリングを組み込んだバージョンを掲載します。

import Foundation

// エラー
enum Errors : Error {
    case NoInputLines // 行数指定が空行だった
    case NoInputEachLine // データの行が空行だった
    case InvalidLines // 行数指定が不正(0以下)
    case NotNumber // 行数指定が数値でない
    case NotEnoughArgument // 項目が2つ以上指定されていない
}

// エラーメッセージ
extension Errors: LocalizedError {
    var errorDescription: String? {
        switch self {
            case .NoInputLines: return "行数指定が空行だった"
            case .NoInputEachLine: return "データの行が空行だった"
            case .InvalidLines: return "行数指定が不正(0以下)"
            case .NotNumber: return "行数指定が数値でない"
            case .NotEnoughArgument: return "項目が2つ以上指定されていない"
        }
    }
}

// main() /////////////////////////////////////////////////
do {
    // 行数を取得(変換した数値が0以下ならば終了)
    let numInputLines = try readInputLines()
    for _ in 1...numInputLines {
        // 1行読み込んで出力する
        try procEachLines()
    }
} catch let err {
    print("ERROR:\(err.localizedDescription)")
    exit(-1)
}


// 1行入力(空なら終了) ////////////////////////////////////////////
func readInputLines() throws -> Int {
    guard let line = readLine() else {
        throw Errors.NoInputLines
    }
    // 入力された行を数値に変換(変換できなければ終了)
    guard let num = Int(line.trimmingCharacters(in: .whitespaces)) else {
        throw Errors.NotNumber
    }
    guard num > 0  else {
        throw Errors.InvalidLines
    }
    return num
}

// 処理のメイン部分 ///////////////////////////////////////////////
func procEachLines() throws {
    // let s = readLine()!.components(separatedBy: " ")
    // 1行読み込み
    guard let line = readLine() else {
        throw Errors.NoInputEachLine
    }
    // 空白で分ける(トリミングする)
    let items = line.trimmingCharacters(in: .whitespaces).components(separatedBy: " ")
    // 分割した項目が2つ以上あるかチェック
    guard items.count > 1 else {
        throw Errors.NotEnoughArgument
    }
    // 出力
    print("hello = \(items[0]), world = \(items[1])")
}

2022-07-13技術swift,プログラミング言語

Posted by woinary