任意の型を格納可能なinterface{}
静的なコンパイル言語であるGoでは、mapのキー・バリューの型を指定する必要がある(map [int] string、map [string] stringなど)。しかし、Goのmapは、バリューに任意の型の値を格納するコレクションも作成できる。それは、Goでは、すべての型が空のインターフェイス「interface{}」を持っているとされることを利用する(すなわち、バリューの型に、「interface{}」を指定する)。
「interface{}」をバリューとして持つ以下の例を見ていこう。
package main import ("fmt") //ユーザー定義のsymbol型 type symbol string var Age ,Name ,Weight symbol func main() { yamada := map[*symbol] interface{} { &Age : 17, &Name : "山田太郎", &Weight : 98.3 }; fmt.Print(yamada) }
map[0x4513f4:17 0x447654:98.3 0x4476d0:山田太郎]
main関数内で、「map[*symbol] interface{}」というmapを定義している。このmapは、キーがsymbol型へのポインタ、バリューが空のインターフェイス「interface{}」である。
このうち、symbol型とはプログラムの冒頭部で定義されたものであり、Age ,Name ,Weightの3つの変数がsymbol型として宣言されている。
mapを初期化する複合リテラルの部分を見てほしい。
{ &Age : 17, &Name : "山田太郎", &Weight : 98.3 }
キーの部分にはsymbol型のポインタ&Age ,&Name ,&Weightが、バリューの部分には整数、文字列、実数が、それぞれ記述されている。この部分だけ見ると、Goのmapは、動的言語であるRubyのハッシュに負けない簡潔な表現ができることが分かるだろう。ちなみにRubyのハッシュで同等の表現を書くと以下の通りとなる。
{ :age => 17, :name => '山田太郎', :weight => 98.3 }
実行結果では、ハッシュを代入した変数yamadaの内容がプリントされている。その内容から、map型であること、キーの中身はポインタが指すアドレス(「0x」から始まる十六進法のアドレス)であること、バリューは整数、文字列、実数がそのまま格納されていることが分かるだろう。
さて、空のインターフェイス「interface{}」が、任意の型を持つ値が格納できる点は、どのように理解すれば良いのだろうか? 継承ベースのオブジェクト指向プログラミングに親しんでいる方は、全ての型の源となるルート型のようなものを想像したいかもしれない。しかし、クラス階層と継承の仕組みを持たないGoでは、そのように理解することはできない。
筆者は、インターフェイスをその字義通りに「外部との接点」と捉えた上で、「空のインターフェイス」とは、「外部との接点を持ちうること」と考えるのが良いのではと考えている。すなわち、Goでは、全ての型から生成されるオブジェクトは、自身のメモリ空間内にそれぞれの型に応じた内部状態(値)を持つと共に、外部から何らかのアクセスを受け入れると「期待」される。この「期待」が、空のインターフェイス「interface{}」として表現されているため、空のインターフェイスには任意の型を代入できると、理解するのである(*3)。
むろん、単に、「interface{}は、任意の型を入れることのできる入れ物である」と理解していても、実用上は問題ない。
interface{}から型情報を抽出する
さて、上記のプロクラムにはいくつか気持ちが悪い点がある。
第1に、symbol型の変数を定義し、そのポインタを用いているにも関わらず、中身は全く代入されていない点である。第2に、mapからキー・バリューを取り出していない点である。
次のプログラムでこの点を解決しよう。
package main import ("fmt") //ユーザー定義のsymbol型 type symbol string //symbol型のメソッドGet func (this symbol) Get(in interface{}) string { return fmt.Sprintf("%v%v",in,this) } var Age ,Name ,Height ,Weight symbol = "歳","さん","(cm)","(Kg)" //mapデータ attr var attr = map[*symbol] interface{} { &Name : "山田太郎", &Age : 17, &Height : 177.3, &Weight : 98.3 } func isUnderAge(age interface{}) bool{ if val,ok :=age.(int); ok { if val < 20 {return true} } return false } func main() { for k,v := range attr { fmt.Println(k.Get(v)) } if isUnderAge(attr[&Age]) { fmt.Printf("%vは未成年\n",Name.Get(attr[&Name])) } }
177.3(cm) 98.3(Kg) 山田太郎さん 17歳 山田太郎さんは未成年
今度のプログラムでは、symbol型の変数にそれぞれの「単位」のようなものを代入している。
Goでは、同一の型を持つ複数の変数を、以下のように、一気に初期化できる。
var Age ,Name ,Height ,Weight symbol = "歳","さん","(cm)","(Kg)"
また、symbol型のメソットGetも定義している。Getの引数はinterface{}であるため、任意の型を受け取れる。Getの返り値は、これをfmt.Sprintf関数で、%vオプションを用いて文字列へと変換し、それと、それぞれの変数が持つ「単位」を結合したものである(実行結果を見れば一目瞭然だろう)。
main関数内では、for〜rangeを用いて、それぞれの値をプリントしている。k.Get(v)というところで、symbol型のメソットGetを活用している。また、ポインタ&Ageに年齢を入れるという約束の上で、&Ageに対応する値が20以下、すなわち、未成年かどうかを判断する関数isUnderAgeも用意している。関数isUnderAgeは、以外と複雑だ。これは、関数isUnderAgeの引数がinterface{}であるため、引数が数値と比較可能である(すなわち引数自身も数値である)ことが期待できないためだ。そこで、変数の型がint型であるかどうかを調べ、値を代入する(int)メソッドを呼出し、型の判別結果を変数okに、対応する値をvalに代入することを試みている(ここでは、Goの特徴のひとつである「複数代入」が行われている)。ここで変数okはbool型であり、okがtrueの時にはvalには値が存在する。その場合のみ、valが20以下であるか(未成年かどうか)を判断している。
おわりに
今回のGoのインターフェイスの用い方は、慣れないうちはやや戸惑うようなものかもしれない。ただ、Goは、スクリプト言語に近い記述ができることは体験してもらえたのてはなかろうか。
次回以降、Goのインターフェイスを用いたオブジェクト指向プログラミングの解説をしていきたい。また、「スライス」も取り上げたい。
3/3 |
Index | |
Goでドメイン特化言語も作成可能? | |
Page1 GoでDSL(ドメイン特化言語)? | |
Page2 Goの組込コレクション機能mapを学ぶ | |
Page3 任意の型を格納可能なinterface{} おわりに |
Coding Edgeお勧め記事 |
いまさらアルゴリズムを学ぶ意味 コーディングに役立つ! アルゴリズムの基本(1) コンピュータに「3の倍数と3の付く数字」を判断させるにはどうしたらいいか。発想力を鍛えよう |
|
Zope 3の魅力に迫る Zope 3とは何ぞや?(1) Pythonで書かれたWebアプリケーションフレームワーク「Zope 3」。ほかのソフトウェアとは一体何が違っているのか? |
|
貧弱環境プログラミングのススメ 柴田 淳のコーディング天国 高性能なIT機器に囲まれた環境でコンピュータの動作原理に触れることは可能だろうか。貧弱なPC上にビットマップの直線をどうやって引く? |
|
Haskellプログラミングの楽しみ方 のんびりHaskell(1) 関数型言語に分類されるHaskell。C言語などの手続き型言語とまったく異なるプログラミングの世界に踏み出してみよう |
|
ちょっと変わったLisp入門 Gaucheでメタプログラミング(1) Lispの一種であるScheme。いくつかある処理系の中でも気軽にスクリプトを書けるGaucheでLispの世界を体験してみよう |
|
- プログラムの実行はどのようにして行われるのか、Linuxカーネルのコードから探る (2017/7/20)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。最終回は、Linuxカーネルの中では、プログラムの起動時にはどのような処理が行われているのかを探る - エンジニアならC言語プログラムの終わりに呼び出されるexit()の中身分かってますよね? (2017/7/13)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。今回は、プログラムの終わりに呼び出されるexit()の中身を探る - VBAにおけるFileDialog操作の基本&ドライブの空き容量、ファイルのサイズやタイムスタンプの取得方法 (2017/7/10)
指定したドライブの空き容量、ファイルのタイムスタンプや属性を取得する方法、FileDialog/エクスプローラー操作の基本を紹介します - さらば残業! 面倒くさいエクセル業務を楽にする「Excel VBA」とは (2017/7/6)
日頃発生する“面倒くさい業務”。簡単なプログラミングで効率化できる可能性がある。本稿では、業務で使うことが多い「Microsoft Excel」で使えるVBAを紹介する。※ショートカットキー、アクセスキーの解説あり
|
|