<はじめに>

どうも!Swift勉強中のさとこ(♂)です。
操作ミスってsiri起動後に「あ、違う」って言ったら、
「もう一度チャンスをください」と返されて吹きました。
 
さて、
今回は前回に引き続きログ出力周りについて、皆さんがあまり使ったことないであろう使い方(多分)を詳しくご紹介していきます。
 
○前回記事
Swift 3系、Xcode 8におけるログ出力関連機能を漁ってみた~その1~
○次回記事(2017/08/21更新)
Swift 3系、Xcode 8におけるログ出力関連機能を漁ってみた~その3~
 

<本記事の内容>

【今回の内容】
・標準出力関数の使い方(応用)
 
【前回の内容】
・標準出力関数の使い方(基本)
 
【次回以降の内容】
・NSLogとos_logのご紹介
・さとこの3分カスタマイズ
・Xcodeのデバッガでログ出力

 
ではでは、考察という名の漁り開始!
(相変わらずシンタックスハイライト無しですがご容赦を。

<開発環境>

・Xcode Version 8.3.3
・Swift 3.1

※機会があればSwift 4、Xcode 9 beta 3の動作も参考程度にご紹介します。

 
さて皆さん、またまたprintのお時間です。

<使い方(応用)~print編~>

・print
指定可能な引数が複数ありますのでキーワード毎にご紹介します。

// 定義
func print<Target>(_ items: Any..., separator: String = default, terminator: String = default, to output: inout Target) where Target : TextOutputStream

公式ドキュメントはコチラ

【キーワード】
・item(items)
・separator
・terminator
・output(to:)

let intValue = 100
let strValue = "文字列"
let range = 1...100
let array: [Any] = [intValue, strValue, true]

// item -> 引数に指定した内容を出力(0個以上)
// ","(カンマ)で区切ることで複数データの出力が可能
print(intValue, strValue, array) // 100 文字列 [100, "文字列", true]

// item -> 閉値域を出力
print(range) // 1...100

// item -> 式を展開(計算)して出力
print(intValue * intValue) // 10000

// separator -> itemで指定した値の末尾に表示される文字列を指定
// (item + separator)
print(intValue, strValue, array, separator: "; ") // 100; 文字列; [100; "文字列"; true;]

// terminator -> itemで指定した値の末尾に表示される文字列を指定
// (item + terminator)
for n in 1...5 {
    print(n, terminator: "") // 12345
}

// output(to:) -> itemで指定した値の接頭に「to:」で指定した可変の文字列型変数を追加
// (item + output)
var questionMark = "Question mark is "
print("?", to: &questionMark) // questionMark = Qustion mark is ?

上記のような使い方ができるようです。
 
上から順に見ていきます。

  • item:0個以上の引数を指定
    動作はコメントにあるとおりですね。
    式なども計算後に出力できるようです。
     

  • separator:区切り文字の設定
    複数の引数を指定した際の出力結果(カンマ区切りの例)をよく見ると、ご丁寧に半角スペースで区切ってくれていますよね。
    これはデフォルトで「" "」(半角スペース)が設定されているためです。
     

  • terminator:末尾文字の設定
    実行時に改行されて出力されるのは、terminatorにデフォルトで「\n」(改行)が設定されているためです。
    今回のようにterminatorに「""」(ブランク)を指定している場合は、出力結果の末尾には何も設定されず改行されません。
    separatorに「\n」(改行)を設定しても改行されますが、terminatorの存在意義が無くなってしまうので通常は不要でしょうね。
     

  • output:指定された変数(var)の更新
    指定する際は引数の接頭に「&」(アンパサンド)を付与します。
    今回は詳しく触れませんが、これは「INOUT引数」といって、引数で指定した変数を関数内部で更新し、直接呼び出し元に戻せる機能を持ちます。
    つまり、関数側で戻り値の設定をせずとも、引数の値を直接書き換えて処理を続行できるということになります。
    ただ、当然ですが上書きを許容しない定数letで定義した引数を指定することはできません。
     
    ↓↓↓使い方はこんな感じ↓↓↓

// price = 100
var price: Double = 100

// 戻り値を返す場合
func calculateTax(price: Double) -> Int {
    return Int(price * 1.08)
}
// 呼び出し時
let taxIncluededPrice = calculateTax(price: price)
print(taxIncluededPrice) // taxIncluededPrice = 108

// INOUT引数を使用する場合
func calculateTax(price: inout Double) -> Void {
    price = price * 1.08
}
// 呼び出し時
calculateTax(price: &price)
print(Int(price)) // price = 108

なんだかC言語系のポインタとアドレス、C#の参照渡しに近いですね。
(なんだかんだ詳しく触れてる笑
 
他にも様々な使用法があるので詳しくはご自身でチェックしてみると良いと思います。
 

>>ちなみに...

  • separatorは出力対象が2つ以上の場合のみ適用される
  • outputを使用した場合はログ出力は行われない

といった特徴があります。

// 引数が1つの場合
let param = "区切り文字設定"
print(param, seperator: "-") // 区切り文字設定

てな感じで「-」が意味を成しません笑
 
では、
ここいらでさらっとprintのまとめです!
 

【printにおける使用可能な引数の概要】

引数一覧 printの機能 初期値 説明
_ item ログ出力 なし 引数の指定 *1
separator ログ出力 " "(半角スペース) 指定した区切り文字列の設定
terminator ログ出力 "\n"(改行) 指定した末尾文字列の設定
output 引数更新 *2 指定した引数の値 指定した引数の更新

*1.型を問わず、未指定、複数指定(カンマ区切り)が可能。
*2.output指定時は他の引数と組み合わせて使用した場合でも引数更新が優先されるため、ログ出力は行わない。
 

【printの使用例】

指定引数 使用例:条件(var a = "TEST") 結果
item e.g.1 print(a) TEST
item e.g.2 print(a, 1, true) TEST 1 true
item e.g.3 print(1...5) 1...5
separator print(a, a, seperator: "@") TEST@TEST
terminator for n in 1...3 {print(n, terminator = "")} 123
output e.g.1 print("->UPDATE", to: &a) TEST->UPDATE
output e.g.2 print(1, 2, 3, separator: "-", to: &a) TEST1-2-3
output e.g.3 for n in 1...3 {print(n, terminator: "", to: &a)} TEST123

 
続いてdebugPrintです。

<使い方(応用)~debugPrint編~>

・debugPrint
「print」と同様の機能を持ち、出力時の動作も変わらず…と思いきや、少々異なる動作があったのでそこに焦点を当ててみます。

// 定義
func debugPrint<Target>(_ items: Any..., separator: String = default, terminator: String = default, to output: inout Target) where Target : TextOutputStream

公式ドキュメントはコチラ

【キーワード】
・String(reflecting: item)
→init(reflecting:)に準拠するイニシャライザ
→引数を、詳細な表記法を含め文字列化する
参考:公式ドキュメントより
 
・String(stringInterpolation: item)
→init(stringInterpolation:)に準拠するイニシャライザ
→文字列補完により渡された引数を鎖状に繋ぎ文字列化する
参考:公式ドキュメントより
 
【予備知識】
・「\」(バックスラッシュ)
→コマンド:「option + @」
→Windowsでは円マーク表記
 
参考:Macにおけるバックスラッシュ(\)の入力方法

let intValue = 100
let strValue = "文字列"
let range = 1…100
let array = [intValue, strValue, true]

// 閉値域
debugPrint(range)        // CountableClosedRange(1...100)
debugPrint("\(range)") // "1...100"

// 配列
debugPrint(array)        // [1, "文字列", true]
debugPrint("\(array)") // "[1, \"文字列\", true]"

ん?CountableClosedRange?何それ?って内容が出力されましたね。

・CountableClosedRange(閉値域)型:x…y
→定義域(x)から値域(y)までの変域の一種
例)インデックスのx~yに相当し、x番目からy番目までカウントの意。

以下の記事に詳しい解説がありますので掘り下げたい方はご参照ください。
Swift 3のRange徹底解説

 
「print」との差異はこんな感じ。
1. 閉値域(x…y)等の型を出力する。
2. 文字列埋め込みによって、引数の持つ機能の型名が出力されない。
3. 文字列埋め込みによって、配列(Array型)や辞書(Dictionary型)では、内部に存在する文字列が「\」で囲われる。
 
前回記事」の基本動作でも確認しましたが、「debugPrint」は文字列埋め込みで出力する際は出力結果が「"」で囲われます。
つまり、文字列として出力される訳です。
閉値域においては「CountableClosedRange」の型が出力されず、引数で受け取った値を文字列として出力しています。
 

>>いったい何が起きているのか?

 

  • 1.「String(reflecting: item)」というイニシャライザが機能している
    キーワードにもありますが、このイニシャライザは引数を詳細な表記法と共に文字列化してくれる機能を持ちます。
     

  • 2.文字列埋め込み時に機能する「String(stringInterpolation: item) 」の処理の後に、「String(reflecting: item)」が行われている
    ざっくり言うとこんな感じ。
    1:単純に文字列化される→("1…100")
    (閉値域がただの文字列になる
    2:詳細な表記法ありで文字列化される→("1…100")
    (文字列が文字列であることを明示的に表記される (「""」が付与される)
    ※String(stringInterpolation: item) は奥が深いため、次回以降で別途ご紹介したいと思います。
     

  • 3.エスケープシーケンスとして機能している
    エスケープ文字「\」が「"」の前に付与される状態になります。
    e.g.)こんな感じ「\"文字列\"」

多くのプログラミング言語でこの書式となっている訳ですが、Swiftも、そもそもプログラミングも初心者ですって方には、一体何のこっちゃって感じだと思いますので少し解説します。
(読み飛ばし可
 

// e.g.
debugPrint("\(["文字列", 100, true])") // "[\"文字列\", 100, true]"

上記の配列内に存在するString型の値は「"」で囲われています。
今回は「"」に対しても同様の処理がなされ、直前に「\」が付与されました。
あれ?「"」が付与されて「""」になるんじゃないの?
(そうならない理由を後述します。
 
そもそも文字列化の際は、コンピュータが文字を文字列と理解できるように、対象となる文字を「"」で囲う必要があります。
ですが、その文字を囲う「"」が並んでしまうとコンピュータはどれが文字列であるのかを判別出来なくなってしまいます。
e.g.1)「""」 → 「"""」
→どれを文字列にしたいのかわからん!ってなる。
e.g.2)「"文字列"」 → 「""文字列""」
→両端のブランクを文字列とみなし、「文字列」自体は未定義の変数のように扱われ、結果エラーになる。
 
その回避策はと言えば「"」の持つ文字列化の機能を無効化すること。
 
では、どのように無効化するか。
「"」を文字列としてみなすため、代替文字として「\」(バックスラッシュ)を使います。
e.g.)「"\""」→「"""」
このように文字列としたい「"」の直前に「\」付与することで可能となります。
これはエスケープシーケンスと呼ばれる書式です。
巷でよく見かける「\n」や「\t」もお仲間です。

【「"」(ダブルクォート)について】
・デフォルトでは、対象の文字を囲う機能を持つ
・「"」を単なる文字として扱う場合、「"」の代替文字は「\」となる

以下の記事に説明がありますのでご参照ください。
文字列とエスケープシーケンスについて | Swift入門編
 

>>ちなみに...

  • 配列の中の文字列を直接出力する際は、「\」では囲われない
// e.g.
debugPrint("\(array[1])") // "文字列"

この辺の明らかに不要な処理に対する考慮はされているようですね。
 
では「print」と大差ありませんが一応まとめ。
 

【debugPrintにおける使用可能な引数の概要】

引数一覧 debugPrintの機能 初期値 説明
_ item ログ出力 なし 引数の指定 *1
separator ログ出力 " "(半角スペース) 指定した区切り文字列の設定
terminator ログ出力 "\n"(改行) 指定した末尾文字列の設定
output 引数更新 *2 指定した引数の値 指定した引数の更新

*1.型を問わず、未指定、複数指定(カンマ区切り)が可能。
*2.output指定時は他の引数と組み合わせて使用した場合でも引数更新が優先されるため、ログ出力は行わない。
 

【debugPrintの使用例】

指定引数 使用例:条件(var a = "TEST") 結果
item e.g.1 debugPrint(a) "TEST"
item e.g.2 debugPrint(a, 1, true) "TEST" 1 true
item e.g.3 debugPrint(1...5) CountableClosedRange(1...5)
separator debugPrint(a, a, separator: "@") "TEST"@"TEST"
terminator for n in 1...3 {debugPrint(n, terminator = "")} 123
output e.g.1 debugPrint("->UPDATE", to: &a) TEST->UPDATE
output e.g.2 debugPrint(1, 2, 3, separator: "-", to: &a) TEST1-2-3
output e.g.3 for n in 1...3 {debugPrint(n, terminator: "", to: &a)} TEST123

 
そして、待ってましたよdumpさん!

<使い方(応用)~dump編~>

・dump
クラス、構造体、列挙型、タプル型などのインスタンス情報を読み出し出力する機能を持ちます。
また、出力時は最下層の出力内容の接頭に「-」(インデント)が付与されます。
 
少々容量が多いので、Part1~3に分けてご紹介します。
※Part1、Part2は「前回記事」の復習を兼ねてます。
 

- Part 1

・データ構造情報の出力1

let intValue = 100
let strValue = "文字列"
let range = 1…100
let array = [intValue, strValue, true]

// 閉値域
dump(range)
// 出力結果
▿ CountableClosedRange(1...100)
  - lowerBound: 1
  - upperBound: 100
// 閉値域"\()"
dump("\(range)")
// 出力結果
- "1...100"

// 配列
dump(array)
// 出力結果
▿ 3 elements
  - 1
  - "文字列"
  - true

// 配列"\()"
dump("\(array)")
// 出力結果
- "[1, \"one\", true]"

「debugPrint」よりも詳細な情報が出力されているのがわかりますね。
閉値域であれば、CountableClosedRangeのlowerBoundとupperBoundが、
配列であれば、要素数に加え、要素が持つ値が階層毎に出力されます。
 
しかし、文字列埋め込みの際は「debugPrint」同様に配列内の文字列が「\」で囲われます。
 

- Part 2

・データ構造情報の出力2
(クラスのインスタンス)

class Person {

    let name: String
    let age: Int
    let sex: String

    init(name: String, age: Int, sex: String) {
        self.name = name
        self.age = age
        self.sex = sex
    }
}
// 呼び出し元
let person = Person(name: "Satoko", age: 26, sex: "男")
dump(person)
// 出力結果
▿ SampleLogTest.Person #0
  - name: "Satoko"
  - age: 26
  - sex: "男"

出力内容がとてもわかり易いですね。
 
単純に「print」や「debugPrint」で出力すると...

print(person) // SampleLogTest.Person
debugPrint(person) // "SampleLogTest.Person"

こうなります。
(うん。Pesronなのは知ってる笑
 
とすると、やはりdumpさんが優秀ですね。
 

- Part 3

・出力情報の整備と出力精度の向上
以下の内容については充実した資料が見つからなかったので、色々試してみた結果を載せます。
(公式にも関数の定義しか載ってないっていうこのhogehoge

// 定義
@discardableResult func dump<T, TargetStream>(_ value: T, to target: inout TargetStream, name: String? = default, indent: Int = default, maxDepth: Int = default, maxItems: Int = default) -> T where TargetStream : TextOutputStream

公式ドキュメントはコチラ

【キーワード】
・_ value
・target(to:)
・name
・indent
・maxDepth
・maxItems

var updateMsg = "TEST: "
// _ value -> 最低ひとつの引数を指定し、出力
// target(to:) ->「to:」で指定した変数の値の末尾に、valueの値を追加
dump(100, to: &updateMsg)
print(updateMsg) // TEST: - 100

let dic: [String:Any] = ["X":1, "Y":2, "Z":3]

// _ value -> valueで指定した値を出力
// name -> nameで指定した文字列をログの「タイトル」として出力
// indent-> indentで指定した数値分の半角スペースを「インデント」として出力
// maxDepth -> コンソールに出力する際の出力データの階層数
// maxItems -> 詳細情報を出力するデータ数

// e.g.1)インデント0、0階層表示、出力データ数1
dump(dic, name: "出力値のタイトル", indent: 0, maxDepth: 0, maxItems: 1)
// 出力結果 なし

// e.g.2)インデント4、2階層表示、出力データ数5の場合
dump(dic, name: "出力値のタイトル", indent: 4, maxDepth: 2, maxItems: 5)

// 出力結果
    ▿ 出力値のタイトル: 3 key/value pairs // maxItems -> 1に該当(※0の場合もこの行は出力され、"▿"(折りたたみ表記)が"▹"で出力される)
      ▿ (2 elements)   // maxItemsでは2に該当
        - key: "X"         // maxItemsでは3に該当
        - value: 1         // maxItemsでは4に該当
      ▿ (2 elements)   // maxItemsでは5に該当 ※ここまでが詳細情報
          (2 children)   // maxItemsでは6以降に該当 ※2つの子要素が存在することは読み取れるが中身を確認できない
        (1 more child) // maxItemsでは6以降に該当 ※もう1つの子要素が存在することは読み取れるが中身を確認できない

var target = "TARGET"
let array = ["X", "Y", "Z"]
// インデント0、3階層表示、出力データ数4の場合
dump(array, to: &target, name: "変更", indent: 0, maxDepth: 3, maxItems: 4)
dump(target)

// 出力結果
- "TARGET▿ 変更: 3 elements\n  - \"X\"\n  - \"Y\"\n  - \"Z\"\n"

うん、ややこしい。
初見では使うの躊躇いますね。
 
とりあえず、
1. valueは最低一つの引数を要する
2. targetは「print」のoutputと同等の動作
3. その他の各引数においては、出力内容の見栄え調整
といったところでしょうか。
 
見易さ使い易さは個人で変わると思いますが、ひとつのデータの内部情報をより詳しく知りたい場合は便利そうです。
(でも相当細部まで出力したい情報がない限りは使わないかなーと思ったり思わなかったり。
 
Part3でご紹介した使い方は特別覚える必要ないと思いますが、どこかで必要としてくれる誰かの為になったら幸いです。
(ほんと。
 
さてさて、小難しいdumpさんのまとめです!
 

【dumpにおける使用可能な引数の概要】

引数一覧 dumpの機能 初期値 説明
_ value ログ出力 なし 指定した値の構造情報出力 *1
target 引数更新 *2 指定した引数の値 指定した引数の更新
name ログ出力 "" 出力情報のタイトルの指定
indent ログ出力 0 インデント数(半角スペース)の指定
maxDepth ログ出力 指定した引数の階層数 出力情報の階層数の指定
maxItems ログ出力 指定した引数の出力行数 出力情報の行数の指定

*1.最低1つの引数が必須となり、複数指定は不可。
*2.target指定時は他の引数と組み合わせて使用した場合でも引数更新が優先されるため、ログ出力は行わない。
 

【dump使用例】

指定引数 使用例:条件(var a = ["X":1, "Y":2, “Z”:3]、var b = "TEST") 出力結果
_ value dump(a["X"]) e.g.1)_ value参照
_ value dump_ a["X"]! e.g.2)_ value参照
target dump(a["X"], to: &b); print(b) e.g.2)target参照
name dump(a["Z"], name: "TITLE") e.g.)name参照
indent dump(a["Z"], indent: 4) e.g.)indent参照
maxDepth dump(a, maxDepth: 1) e.g.)maxDepth参照
maxItems dump(a, maxItems: 5) e.g.)maxItems参照

【dump使用例(出力結果)】

// e.g.1)_ value 
▿ Optional(1)
  - some: 1

// e.g.2)_ value 
- 1

// e.g.)target
TEST▿ Optional(1)
  - some: 1

// e.g.)name
▿ TITLE: Optional(2)
  - some: 2

// e.g.)indent
    ▿ Optional(3)
      - some: 3

// e.g.)maxDepth
▿ 3 key/value pairs
  ▹ (2 elements)
  ▹ (2 elements)
  ▹ (2 elements)

// e.g.)maxItems
▿ 3 key/value pairs
  ▿ (2 elements)
    - key: "X"
    - value: 1
  ▿ (2 elements)
      (2 children)
    (1 more child)

 

<まとめ>

【print】

  • 型を気にせず出力可能であるため、値の確認が容易
  • カンマ区切りにより複数の値を出力可能
  • 区切り文字や終端文字の設定に加え、INOUT引数の機能を用いて、引数の値の更新も可能  

【debugPrint】

  • 「print」と同等の出力機能を持つ
  • String(reflecting: item)により、渡された引数の詳細な表記法を出力可能
  • 引数が文字列埋め込みにより渡された場合、String(reflecting: item)の機能は無効化される  

【dump】

  • 「print」、「debugPrint」よりも詳細な構造情報を出力可能
  • 指定引数の追加により出力内容の整備(見栄えの調整)が可能
  • debugPrintと同様、引数が文字列埋め込みにより渡された場合、詳細情報は出力不可

 

<おわりに>

意外と知らない標準出力御三家の一面を知って頂けたようなら幸いです。
ログ出力とは言いつつも変数の値の更新もやってくれたり、
思いの外詳細な情報も出力してくれたりと守備範囲の広さに驚かされましたね。
 
次回は「NSLog」、「os_log」についてご紹介します。 
3分カスタマイズも乞うご期待!
 
以上!

Shere
  • はてなブログ
  • Twitter
  • Facebook
Swift 3系、Xcode 8におけるログ出力関連機能を漁ってみた~その2~

Writer

  • Name

    さとこ

  • Position

    どこにもあと2歩くらい足りないシャイなエンジニア

  • Profile

    C/C++/C#/Objective-C/Swift/Java/PHP,Install Shiled Script言語/Oracle DB等。 触りましましたよ、ちょんちょんって。 Swiftなう。みなさんアプリ作りましょ!