読者です 読者をやめる 読者になる 読者になる

golangでコマンドラインツールを作る #6 interface{}をstructにマッピングする

f:id:masato47744:20150816150451p:plain

その5の続き

PBXFileReference

isaが特定できるようになったので、どれか一つのisaのjsonをパースしてみる。 例えば、なんのファイルがあるかのisaはPBXFileReferenceでこれは必ずこういう形をしてる。(※進めていくうちに気づいたけどしてなかった。)

{
      "path" : "AppDelegate.swift",
      "isa" : "PBXFileReference",
      "lastKnownFileType" : "sourcecode.swift",
      "sourceTree" : "<group>"
}

これなら型にできそう。

golangの型

golangにはクラスという概念がないっぽい。その代わりに構造体を使って型を作れるって感じで合ってるのかな? よく分からないけどそんな雰囲気と仮置きしておいてとりあえず先に進む。

構造体を定義する。

type FileReference struct {
    path              string
    lastKnownFileType string
    sourceTree        string
}

構造体を初期化するやり方は色々あるようだ。これが参考になる。[Go] 構造体の初期化方法まとめ - Qiita で、初期化しようと思ってるんだけど、ループで回してるバリューがinterface{}なのでキャスト、どうすればいいかわからない。 ので、調べてみる。

interface{}のキャスト

interface{}をある型として扱うっていうやつなんかうまくいってたから、適当にv.(string)とかやってたけど これがまさにそうだったらしい。ここにやり方まとまってる。Golang: interface{}, type assertions and type switches 全部で3パターンやり方がある。 俺が今までやってたのは危険なキャストの仕方を使ってたようだ。間違えると、panicを起こす。 panic: interface conversion: interface is string, not map[string]interface {} 安全なキャストは、第2返却値をもらうようにする。キャストがうまくいかなかったかが分かる。 もう一つはswitchで考えうる限りの型チェックもできる。TPOによって使えばよさそう。

var a interface{} = "string"
s := a.(string) // キャストできたかチェックできない
s, ok := a.(string) // okにtrue、falseが帰ってきてキャストできなければチェックできる
switch v := a.(type) {
  case string:
    fmt.Println(v)
  case int32, int64:
    fmt.Println(v)
  default:
    fmt.Println("unknown")
}

swiftのas?に似てるかもしれない。

let a: AnyObject = "string"
if let s = a as? String {
    println(s)
} else if let i = a as? Int {
    println(i)
}

で、いざやってみたらやっぱりPBXFileReferenceは決まった型じゃないことが分かった。 mapのkeyがないとpanicになるようだ。 これも、さっきのキャストみたいに、第2返り値で安全にチェックできるようだ。 ということで新しい型を定義

type FileReference struct {
    name              string
    path              string
    lastKnownFileType string
    includeInIndex    string
    explicitFileType  string
    sourceTree        string
}

そして、mapでとるときに安全にとる。安全にとりつつなければデフォルト値とかってmapでできるのかな? なんかちゃっと調べたけどなさそう。なので、これも自分で書く。 ジェネリクスみたいのできないのかな。

func lookupStr(m map[string]interface{}, k string) string {
    if v, found := m[k]; found {
        if s, ok := v.(string); ok {
            return s
        }
    }
    return ""
}

そして、ようやくそれっぽいのができた。

コマンドラインのフラグで分ける

で、今のままだとセクションを指定してその情報を見るみたいなことができない。 こうやりたい。

$ xgodeproj show project.pbxproj --section PBXFileReference

どうやら、これはcodegangsta/cliの仕組みなのかgolangの仕組みか分からないけど、 Flagというやつでできるっぽい。 やり方は簡単。commands.goのFlagsに以下のように追加する。

var Commands = []cli.Command{
    {
        Name:   "show",
        Usage:  "Prints section names or each section information",
        Action: command.CmdShow,
        Flags: []cli.Flag{
            cli.StringFlag{
                Name:  "section",
                Value: "",
                Usage: "section name for pbxproj",
            },
        },
    },
}

使う側では、こんな感じで取得できる。

section := c.String("section")

この辺りを組み合わせて結果的にこうなった。それっぽくなってきたぞ。

$ xgodeproj show project.pbxproj --section PBXFileReference
SampleTests.xctest
Base.lproj/LaunchScreen.xib
Images.xcassets
Base.lproj/Main.storyboard
AppDelegate.swift
Sample.app
Info.plist
Info.plist
SampleTests.swift
ViewController.swift

ここまでの感想

  • interfaceに対して実装する、継承できないっていうのswiftのPOPに似てるかもと思った。
  • キャストのやり方分かった。
  • 安全にやるやり方は、たいてい、xxx, ok := process() みたいな感じってことが分かってきた
  • switchが便利だ。if-elseの代わりになる
  • structをnewするやり方がいっぱいあるのは逆にやめてほしい

その7