リフレクション

Reflection

この章のすべてのコードはここにあります

Twitter

golangチャレンジ:構造体xを受け取り、内部にあるすべての文字列フィールドに対してfnを呼び出す関数walk(x interface{}, fn func(string))を記述します。難易度:再帰的に。

これを行うには、リフレクション(reflection)を使用する必要があります。

コンピューティングにおけるリフレクションは、プログラムが、特にタイプを通じて、独自の構造を調べる能力です。それは一種のメタプログラミングです。また、混乱の元にもなります。

The Go Blog: Reflection抜粋

「インターフェース(interface)」とは何ですか?

Goでは、stringintなどの既知の型や、BankAccountなどの独自の型で機能する関数の点で、タイプの安全性を提供してきました。

つまり、自由な値(ドキュメント)を取得し、間違った型を関数に渡そうとするとコンパイラーが文句を言います。

コンパイル時に型がわからない関数を書きたいというシナリオに出くわすかもしれません。

Goでは、これを any 型と考えることができる型interface{}で回避できます。

したがって、walk(x interface{}, fn func(string))は、xの任意の値を受け入れます。

では、すべてに「インターフェース」を使用し、本当に柔軟な機能を持たないのはなぜでしょうか?

  • 「インターフェースinterface」をとる関数のユーザーとして、タイプの安全性を失います。タイプstringFoo.barを関数に渡すつもりでしたが、代わりにintであるFoo.bazを渡した場合はどうなりますか?コンパイラーは間違いを通知できません。また、関数に渡すことが許可されている what もわかりません。たとえば関数が UserServiceをとることを知ることは非常に便利です。

  • そのような関数の書き方として、渡された anything を検査して、型が何であり、それで何ができるのかを理解する必要があります。これは、リフレクション(reflection)を使用して行われます。これは非常に不格好で読みにくい場合があり、実行時にチェックを行う必要があるため一般的にパフォーマンスが低下します。

つまり、本当に必要な場合にのみ、リフレクションを使用してください。

ポリモーフィック関数が必要な場合は、ユーザーが関数を機能させるために必要なメソッドを実装している場合に、ユーザーが複数の型で関数を使用できるように、(interfaceではなく混乱を防ぐために)の周囲で設計できるかどうか検討してください。

私たちの機能は、さまざまなことを処理できる必要があります。いつものように、サポートしたい新しいものごとにテストを作成し、完了するまでリファクタリングを繰り返すというアプローチをとります。

最初にテストを書く

文字列フィールドが(x)に含まれている構造体で関数を呼び出す必要があります。 次に、渡された関数(fn)をスパイして、呼び出されているかどうかを確認できます。

func TestWalk(t *testing.T) {

    expected := "Chris"
    var got []string

    x := struct {
        Name string
    }{expected}

    walk(x, func(input string) {
        got = append(got, input)
    })

    if len(got) != 1 {
        t.Errorf("wrong number of function calls, got %d want %d", len(got), 1)
    }
}
  • どの文字列がwalkによってfnに渡されたかを格納する文字列のスライス(got)を格納したいと思います。多くの場合、前の章では、関数/メソッドの呼び出しをスパイするために専用の型を作成しましたが、この場合は、gotを閉じるfnの匿名関数を渡すだけです。

  • 最も単純な「ハッピー」パスを取得するために、文字列型のNameフィールドを持つ匿名のstructを使用します。

  • 最後に、walkxとスパイで呼び出し、今のところgotの長さを確認するだけです。非常に基本的な動作が得られたら、アサーションでより具体的になります。

テストを実行してみます

./reflection_test.go:21:2: undefined: walk

テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します

walkを定義する必要があります

func walk(x interface{}, fn func(input string)) {

}

テストを再試行してください

=== RUN   TestWalk
--- FAIL: TestWalk (0.00s)
    reflection_test.go:19: wrong number of function calls, got 0 want 1
FAIL

成功させるのに十分なコードを書く

このパスを作成するために、任意の文字列でスパイを呼び出すことができます。

func walk(x interface{}, fn func(input string)) {
    fn("I still can't believe South Korea beat Germany 2-0 to put them last in their group")
}

これでテストに合格するはずです。 次に行う必要があるのは、fnの呼び出し対象をより具体的にアサートすることです。

最初にテストを書く

次のコードを既存のテストに追加して、fnに渡された文字列が正しいことを確認します

if got[0] != expected {
    t.Errorf("got %q, want %q", got[0], expected)
}

テストを実行してみます

=== RUN   TestWalk
--- FAIL: TestWalk (0.00s)
    reflection_test.go:23: got 'I still can't believe South Korea beat Germany 2-0 to put them last in their group', want 'Chris'
FAIL

成功させるのに十分なコードを書く

func walk(x interface{}, fn func(input string)) {
    val := reflect.ValueOf(x)
    field := val.Field(0)
    fn(field.String())
}

このコードは 非常に安全でナイーブ ですが、「赤」(テスト失敗)にあるときの目標は、可能な限り最小限のコードを記述することです。次に、懸念に対処するためのテストをさらに記述します。

リフレクションを使用してxを確認し、そのプロパティを確認する必要があります。

reflect packageには、指定された変数のValueを返す関数ValueOfがあります。これには、次の行で使用するフィールドなど、値を検査する方法があります。

次に、渡された値について非常に楽観的な仮定を行います。

  • 最初で唯一のフィールドを見て、パニックを引き起こすフィールドがまったくない場合があります。

  • 次に、基になる値を文字列として返すString()を呼び出しますが、フィールドが文字列以外の場合は間違っていることがわかります。

リファクタリング♪

私たちのコードは単純なケースに合格していますが、コードには多くの欠点があることを知っています。

さまざまな値を渡し、fnが呼び出された文字列の配列をチェックするいくつかのテストを作成します。

新しいシナリオのテストを続行しやすくするために、テストをテーブルベースのテストにリファクタリングする必要があります。

func TestWalk(t *testing.T) {

    cases := []struct{
        Name string
        Input interface{}
        ExpectedCalls []string
    } {
        {
            "Struct with one string field",
            struct {
                Name string
            }{ "Chris"},
            []string{"Chris"},
        },
    }

    for _, test := range cases {
        t.Run(test.Name, func(t *testing.T) {
            var got []string
            walk(test.Input, func(input string) {
                got = append(got, input)
            })

            if !reflect.DeepEqual(got, test.ExpectedCalls) {
                t.Errorf("got %v, want %v", got, test.ExpectedCalls)
            }
        })
    }
}

これで、シナリオを簡単に追加して、複数の文字列フィールドがある場合にどうなるかを確認できます。

最初にテストを書く

次のシナリオをcasesに追加します。

{
    "Struct with two string fields",
    struct {
        Name string
        City string
    }{"Chris", "London"},
    []string{"Chris", "London"},
}

テストを実行してみます

=== RUN   TestWalk/Struct_with_two_string_fields
    --- FAIL: TestWalk/Struct_with_two_string_fields (0.00s)
        reflection_test.go:40: got [Chris], want [Chris London]

成功させるのに十分なコードを書く

func walk(x interface{}, fn func(input string)) {
    val := reflect.ValueOf(x)

    for i:=0; i<val.NumField(); i++ {
        field := val.Field(i)
        fn(field.String())
    }
}

valには、値のフィールド数を返すメソッドNumFieldがあります。 これにより、フィールドを反復処理し、テストに合格したfnを呼び出すことができます。

リファクタリング♪

ここにコードを改善する明白なリファクターがあるようには見えないので、続けましょう。

walkの次の欠点は、すべてのフィールドがstringであると想定していることです。 このシナリオのテストを書いてみましょう。

最初にテストを書く

次のケースを追加

{
    "Struct with non string field",
    struct {
        Name string
        Age  int
    }{"Chris", 33},
    []string{"Chris"},
},

テストを実行してみます

=== RUN   TestWalk/Struct_with_non_string_field
    --- FAIL: TestWalk/Struct_with_non_string_field (0.00s)
        reflection_test.go:46: got [Chris <int Value>], want [Chris]

成功させるのに十分なコードを書く

フィールドのタイプがstringであることを確認する必要があります。

func walk(x interface{}, fn func(input string)) {
    val := reflect.ValueOf(x)

    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)

        if field.Kind() == reflect.String {
            fn(field.String())
        }
    }
}

そのKindをチェックすることでそれを行うことができます。

リファクタリング♪

繰り返しになりますが、コードは今のところ十分に妥当です。

次のシナリオは、「フラット」な「構造体」でない場合はどうなるのでしょうか。 言い換えると、いくつかのネストされたフィールドを持つstructがあるとどうなりますか?

最初にテストを書く

私たちは匿名構造体構文を使用して、テストのためにアドホックに型を宣言しているので、そのように続けることができます

{
    "Nested fields",
    struct {
        Name string
        Profile struct {
            Age  int
            City string
        }
    }{"Chris", struct {
        Age  int
        City string
    }{33, "London"}},
    []string{"Chris", "London"},
},

しかし、内部の匿名構造体を取得すると、構文が少し乱雑になることがわかります。構文を改善するために作成する提案があります

このシナリオの既知のタイプを作成してこれをリファクタリングし、テストで参照してみましょう。 私たちのテストのコードの一部がテストの外にあるという点で少し間接的ですが、読者は初期化を見て、structの構造を推測できるはずです。

次の型宣言をテストファイルのどこかに追加します。

type Person struct {
    Name    string
    Profile Profile
}

type Profile struct {
    Age  int
    City string
}

これをケースに追加して、以前よりもはるかに明確に読み取ることができます。

{
    "Nested fields",
    Person{
        "Chris",
        Profile{33, "London"},
    },
    []string{"Chris", "London"},
},

テストを実行してみます

=== RUN   TestWalk/Nested_fields
    --- FAIL: TestWalk/Nested_fields (0.00s)
        reflection_test.go:54: got [Chris], want [Chris London]

問題は、型の階層の最初のレベルのフィールドでのみ反復していることです。

成功させるのに十分なコードを書く

func walk(x interface{}, fn func(input string)) {
    val := reflect.ValueOf(x)

    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)

        if field.Kind() == reflect.String {
            fn(field.String())
        }

        if field.Kind() == reflect.Struct {
            walk(field.Interface(), fn)
        }
    }
}

解決策は非常に簡単です。そのKindをもう一度調べ、それが「構造体struct」である場合は、その内部の「構造体struct」でもう一度walkを呼び出すだけです。

リファクタリング♪

func walk(x interface{}, fn func(input string)) {
    val := reflect.ValueOf(x)

    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)

        switch field.Kind() {
        case reflect.String:
            fn(field.String())
        case reflect.Struct:
            walk(field.Interface(), fn)
        }
    }
}

同じ値の比較を複数回行う場合、「一般的に」switchにリファクタリングすると、読みやすさが向上し、コードの拡張が容易になります。

渡された構造体の値がポインターの場合はどうなりますか?

最初にテストを書く

このケースを追加

{
    "Pointers to things",
    &Person{
        "Chris",
        Profile{33, "London"},
    },
    []string{"Chris", "London"},
},

テストを実行してみます

=== RUN   TestWalk/Pointers_to_things
panic: reflect: call of reflect.Value.NumField on ptr Value [recovered]
    panic: reflect: call of reflect.Value.NumField on ptr Value

成功させるのに十分なコードを書く

func walk(x interface{}, fn func(input string)) {
    val := reflect.ValueOf(x)

    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }

    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)

        switch field.Kind() {
        case reflect.String:
            fn(field.String())
        case reflect.Struct:
            walk(field.Interface(), fn)
        }
    }
}

ポインターValueNumFieldを使用することはできません。Elem()を使用する前に、基になる値を抽出する必要があります。

リファクタリング♪

与えられた interface{}から reflect.Valueを関数に抽出する責任をカプセル化しましょう。

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)

        switch field.Kind() {
        case reflect.String:
            fn(field.String())
        case reflect.Struct:
            walk(field.Interface(), fn)
        }
    }
}

func getValue(x interface{}) reflect.Value {
    val := reflect.ValueOf(x)

    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }

    return val
}

これは実際には more コードを追加しますが、抽象化レベルは適切だと思います。

  • 検査できるように、xreflect.Valueを取得します。方法は気にしません。

  • フィールドを反復処理し、そのタイプに応じて必要なことをすべて実行します。

次に、スライスをカバーする必要があります。

最初にテストを書く

{
    "Slices",
    []Profile {
        {33, "London"},
        {34, "Reykjavík"},
    },
    []string{"London", "Reykjavík"},
},

テストを実行してみます

=== RUN   TestWalk/Slices
panic: reflect: call of reflect.Value.NumField on slice Value [recovered]
    panic: reflect: call of reflect.Value.NumField on slice Value

テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します

これは以前のポインターシナリオに似ています。reflect.ValueNumFieldを呼び出そうとしていますが、構造体ではないため、これはありません。

成功させるのに十分なコードを書く

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    if val.Kind() == reflect.Slice {
        for i:=0; i< val.Len(); i++ {
            walk(val.Index(i).Interface(), fn)
        }
        return
    }

    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)

        switch field.Kind() {
        case reflect.String:
            fn(field.String())
        case reflect.Struct:
            walk(field.Interface(), fn)
        }
    }
}

リファクタリング♪

これは機能しますが、不幸です。心配はいりません。 テストに裏打ちされた実際のコードがあるので、好きなように自由に変更することができます。

少し抽象的に考えると、どちらかでwalkと呼びたい

  • 構造体の各フィールド

  • スライス内の各 thing

現時点でのコードはこれを実行しますが、十分に反映していません。 最初に、それがスライス(残りのコードの実行を停止するための return付き)であるかどうかを確認し、そうでない場合は構造体であると想定します。

コードを書き直して、代わりにタイプ first を確認してから作業を行います。

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    switch val.Kind() {
    case reflect.Struct:
        for i:=0; i<val.NumField(); i++ {
            walk(val.Field(i).Interface(), fn)
        }
    case reflect.Slice:
        for i:=0; i<val.Len(); i++ {
            walk(val.Index(i).Interface(), fn)
        }
    case reflect.String:
        fn(val.String())
    }
}

格好良く!

構造体またはスライスの場合は、それぞれの値に対してwalkを呼び出してその値を繰り返し処理します。 それ以外の場合、reflect.Stringであれば、fnを呼び出すことができます。

それでも、私にはそれがより良いものになり得るような気がします。フィールド/値を反復してwalkを呼び出すという操作の繰り返しがありますが、概念的には同じです。

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    numberOfValues := 0
    var getField func(int) reflect.Value

    switch val.Kind() {
    case reflect.String:
        fn(val.String())
    case reflect.Struct:
        numberOfValues = val.NumField()
        getField = val.Field
    case reflect.Slice:
        numberOfValues = val.Len()
        getField = val.Index
    }

    for i:=0; i< numberOfValues; i++ {
        walk(getField(i).Interface(), fn)
    }
}

valuereflect.Stringの場合、通常のようにfnを呼び出すだけです。

それ以外の場合、switchはタイプに応じて2つのものを抽出します

  • いくつのフィールドがありますか

  • Valueを抽出する方法(FieldまたはIndex

それらを決定したら、getField関数の結果を使用して、numberOfValueswalkを呼び出して反復できるようにします。

これで完了です。配列の処理は簡単です。

最初にテストを書く

ケースに追加

{
    "Arrays",
    [2]Profile {
        {33, "London"},
        {34, "Reykjavík"},
    },
    []string{"London", "Reykjavík"},
},

テストを実行してみます

=== RUN   TestWalk/Arrays
    --- FAIL: TestWalk/Arrays (0.00s)
        reflection_test.go:78: got [], want [London Reykjavík]

成功させるのに十分なコードを書く

配列はスライスと同じように処理できるため、コンマを使用してケースに追加するだけです。

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    numberOfValues := 0
    var getField func(int) reflect.Value

    switch val.Kind() {
    case reflect.String:
        fn(val.String())
    case reflect.Struct:
        numberOfValues = val.NumField()
        getField = val.Field
    case reflect.Slice, reflect.Array:
        numberOfValues = val.Len()
        getField = val.Index
    }

    for i:=0; i< numberOfValues; i++ {
        walk(getField(i).Interface(), fn)
    }
}

次に処理するタイプは mapです。

最初にテストを書く

{
    "Maps",
    map[string]string{
        "Foo": "Bar",
        "Baz": "Boz",
    },
    []string{"Bar", "Boz"},
},

テストを実行してみます

=== RUN   TestWalk/Maps
    --- FAIL: TestWalk/Maps (0.00s)
        reflection_test.go:86: got [], want [Bar Boz]

成功させるのに十分なコードを書く

ここでも少し抽象的に考えると、mapstructに非常に似ていることがわかります。これは、コンパイル時にキーが不明であるということだけです。

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    numberOfValues := 0
    var getField func(int) reflect.Value

    switch val.Kind() {
    case reflect.String:
        fn(val.String())
    case reflect.Struct:
        numberOfValues = val.NumField()
        getField = val.Field
    case reflect.Slice, reflect.Array:
        numberOfValues = val.Len()
        getField = val.Index
    case reflect.Map:
        for _, key := range val.MapKeys() {
            walk(val.MapIndex(key).Interface(), fn)
        }
    }

    for i:=0; i< numberOfValues; i++ {
        walk(getField(i).Interface(), fn)
    }
}

ただし、設計により、インデックスからマップから値を取得することはできません。これは key によってのみ行われるため、抽象化を壊します。

リファクタリング♪

今の気分はどうですか?

当初は素晴らしい抽象化のように思われたかもしれませんが、コードは少し不安定に感じられます。

これで問題ありません! リファクタリングは道のりであり、間違いを犯すこともあります。 TDDの主要なポイントは、これらのことを試す自由を私たちに与えることです。

テストに裏打ちされた小さなステップを踏むことによって、これは決して不可逆的な状況ではありません。 リファクタリング前の状態に戻しましょう。

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    walkValue := func(value reflect.Value) {
        walk(value.Interface(), fn)
    }

    switch val.Kind() {
    case reflect.String:
        fn(val.String())
    case reflect.Struct:
        for i := 0; i< val.NumField(); i++ {
            walkValue(val.Field(i))
        }
    case reflect.Slice, reflect.Array:
        for i:= 0; i<val.Len(); i++ {
            walkValue(val.Index(i))
        }
    case reflect.Map:
        for _, key := range val.MapKeys() {
            walkValue(val.MapIndex(key))
        }
    }
}

walkを導入しました。これは、valからreflect.Valueを抽出するだけでよいように、switch内のwalkへの呼び出しを乾燥させます。

最後の問題

Goのマップは順序を保証するものではないことに注意してください。したがって、fnの呼び出しは特定の順序で行われると断言するため、テストが失敗することがあります。

これを修正するには、マップを含むアサーションを、順序を気にしない新しいテストに移動する必要があります。

t.Run("with maps", func(t *testing.T) {
    aMap := map[string]string{
        "Foo": "Bar",
        "Baz": "Boz",
    }

    var got []string
    walk(aMap, func(input string) {
        got = append(got, input)
    })

    assertContains(t, got, "Bar")
    assertContains(t, got, "Boz")
})

assertContainsの定義方法は次のとおりです

func assertContains(t *testing.T, haystack []string, needle string)  {
    t.Helper()
    contains := false
    for _, x := range haystack {
        if x == needle {
            contains = true
        }
    }
    if !contains {
        t.Errorf("expected %+v to contain %q but it didn't", haystack, needle)
    }
}

次に処理したい型はchanです。

最初にテストを書く

t.Run("with channels", func(t *testing.T) {
        aChannel := make(chan Profile)

        go func() {
            aChannel <- Profile{33, "Berlin"}
            aChannel <- Profile{34, "Katowice"}
            close(aChannel)
        }()

        var got []string
        want := []string{"Berlin", "Katowice"}

        walk(aChannel, func(input string) {
            got = append(got, input)
        })

        if !reflect.DeepEqual(got, want) {
            t.Errorf("got %v, want %v", got, want)
        }
    })

テストを実行してみます

--- FAIL: TestWalk (0.00s)
    --- FAIL: TestWalk/with_channels (0.00s)
        reflection_test.go:115: got [], want [Berlin Katowice]

成功させるのに十分なコードを書く

Recv()で閉じられるまで、チャネルを通じて送信されたすべての値を反復処理できます

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    var getField func(int) reflect.Value

    switch val.Kind() {
    case reflect.String:
        fn(val.String())
    case reflect.Struct:
        numberOfValues = val.NumField()
        getField = val.Field
    case reflect.Slice, reflect.Array:
        numberOfValues = val.Len()
        getField = val.Index
    case reflect.Map:
        for _, key := range val.MapKeys() {
            walk(val.MapIndex(key).Interface(), fn)
        }
    case reflect.Chan:
        for v, ok := val.Recv(); ok; v, ok = val.Recv() {
            walk(v.Interface(), fn)
        }
    }
}

次に処理するタイプはfuncです。

最初にテストを書く

t.Run("with function", func(t *testing.T) {
        aFunction := func() (Profile, Profile) {
            return Profile{33, "Berlin"}, Profile{34, "Katowice"}
        }

        var got []string
        want := []string{"Berlin", "Katowice"}

        walk(aFunction, func(input string) {
            got = append(got, input)
        })

        if !reflect.DeepEqual(got, want) {
            t.Errorf("got %v, want %v", got, want)
        }
    })

テストを実行してみます

--- FAIL: TestWalk (0.00s)
    --- FAIL: TestWalk/with_function (0.00s)
        reflection_test.go:132: got [], want [Berlin Katowice]

成功させるのに十分なコードを書く

このシナリオでは、引数のない関数はあまり意味がありません。ただし、任意の戻り値を許可する必要があります。

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    var getField func(int) reflect.Value

    switch val.Kind() {
    case reflect.String:
        fn(val.String())
    case reflect.Struct:
        numberOfValues = val.NumField()
        getField = val.Field
    case reflect.Slice, reflect.Array:
        numberOfValues = val.Len()
        getField = val.Index
    case reflect.Map:
        for _, key := range val.MapKeys() {
            walk(val.MapIndex(key).Interface(), fn)
        }
    case reflect.Chan:
        for v, ok := val.Recv(); ok; v, ok = val.Recv() {
            walk(v.Interface(), fn)
        }
    case reflect.Func:
        valFnResult := val.Call(nil)
        for _, res := range valFnResult {
            walk(res.Interface(), fn)
        }
    }
}

まとめ

  • reflectパッケージのいくつかの概念を導入しました。

  • 任意のデータ構造をたどるために再帰を使用しました。

  • 振り返ってみると、悪いリファクタリングをしましたが、それについてはあまり動揺はありません。テストを反復的に行うことで、それほど大したことではありません。

  • これは、リフレクションの小さな側面だけをカバーしています。GOブログでは、詳細を網羅した優れた記事を掲載しています

  • リフレクションについて理解したので、使用しないように最善を尽くしてください。

最終更新