演習5

演習4のプログラムを次のように変更せよ.

  1. テキストの0.5秒毎の表示に加え,キーボードから文字を入力するごとにその入力された文字を別の領域に表示せよ.
  2. キーボド入力と問題文を比較し,文字入力の度に正しい文字( 同じ文字 )が入力されたか否を判定せよ.その結果を表示せよ.
  3. 問題文に,かな・漢字(ASCII以外の文字)を使えるようにせよ.

解答例(1)

  1. package main
  2. import (
  3. "time"
  4. "bufio"
  5. "os"
  6. "./textbox"
  7. )
  8. const text = "The Go programming language is an open source project to make programmers more productive."
  9. // --- ProblemBox ---
  10. // 問題文を0.5秒ごとに1文字ずつ表示するデータ型.
  11. type ProblemBox struct {
  12. *textbox.TextBox
  13. text string
  14. }
  15. // 位置,大きさ,表示する問題文を指定してProblemBoxを生成する.
  16. func New(x,y,w,h int, s string) (pb *ProblemBox) {
  17. return &ProblemBox{textbox.New(x,y,w,h),s}
  18. }
  19. // 問題文を0.5秒ごとに表示する.
  20. func (pb *ProblemBox) Play() {
  21. pb.Clear()
  22. for i:= 0; i < len(pb.text); i++ {
  23. pb.Putc(pb.text[i])
  24. time.Sleep(5e8)
  25. }
  26. pb.Puts("[Enter]"); // 全て表示したら,最後に[Enter]と表示する.
  27. }
  28. // --- ProblemBox end ---
  29. func main() {
  30. textbox.Clear()
  31. t1 := New(3,3,70,4, text)
  32. go t1.Play()
  33. t2 := textbox.New(3,8,70,4)
  34. keyin := bufio.NewReader(os.Stdin)
  35. for {
  36. c,_:= keyin.ReadByte() // 標準入力から1文字読み込む(ASCII文字のみ可)
  37. if c == '\n' { // [Enter]キーが押されたら終了
  38. break
  39. }
  40. t2.Putc(c)
  41. }
  42. }

解説

ゴルーチン

  • ゴルーチンは関数またはメソッドを同一のアドレス空間で並列に実行する仕掛けです.
  • 関数やメソッドを新規のゴルーチンとして実行するには,呼び出しの直前にキーワードgoを付けます.
  • 41行目の go t1.Play() がゴルーチンの呼出しとなっています.この例では,メソッドt1.Play()が呼び出され,その呼出しの終了を待たずに,並行して42行目以降が実行されます.
  • これによって,t1.Play()が問題文を0.5秒ごとに1文字ずつ表示するのと並行して,ユーザが入力した文字をt2というテキストボックスに表示します.
  • ゴルーチンについての詳細は,言語仕様チュートリアル実践Go言語を参照してください.

bufioパッケージ

  • bufioパッケージは入出力のバッファリング機能やその他便利な機能を提供します.
  • プログラムで利用している関数はNewReader()とReadByte()です.このほかに,UTF-8エンコードされたUnicode文字を一文字読み込むReadRune(),指定したデリミタが現れるまで読み込みを行い,読み込んだ文字列を返すReadString()などの関数を提供します.
  • NewReader()は,io.Readerを引数としてbufio.Readerを返します.45行目のbufio.NewReader(os.Stdin) は,io.Reader型である標準入力os.Stdinを引数として,新たなbufio.Readerを生成しkeyinという変数に代入しています.
  • ReadByte()は,1バイト読み込みその値を返します.読み込むデータがないときはエラーが返ります。47行目のc,_:= keyin.ReadByte() では,標準入力から1バイト読み込みその値をcに代入しています.エラーは無視しています(手抜きです).
  • bufioパッケージの詳細についてはパッケージドキュメントを参照してください.

osパッケージ

  • osパッケージは,オペレーティングシステムの機能へのプラットフォームに依存しないインタフェースを提供します.
  • 44行目のos.Stdinは,osパッケージで定義されている変数で,標準入力を表すファイルディスクリプタです.
  • osパッケージの詳細については,パッケージドキュメントを参照してください.

実行時のTips

  • このプログラムを実行するには,キーボード入力時にバッファリングもエコーもしないように設定する必要があります.
  • プログラムの実行前に,コマンドラインで stty cbreak -echo とすることで,バッファリングオフとエコーオフを設定します.
  • 元に戻すには,コマンドラインで stty sane と入力します(Backspaceキーで削除したいときは,stty erase ^H も必要).

プログラムの動作(main)

  • 38行目:画面全体をクリア.
  • 40行目:ProblemBoxを生成.
  • 41行目:ProblemBoxを再生(ゴルーチンとしてPlay).
  • 43行目:TextBoxを生成.
  • 44行目:標準入力(os.Stdin)からbufio.Readerを生成
  • 46行目:標準入力から1文字入力
  • 47-48行目:入力した文字が改行'\n'なら終了
  • 50行目:入力した文字をテキストボックスに表示

解答例(2)

解答例(1)のmain関数のみ,次のように変更します.この部分の解説は不要だと思います.

  1. func main() {
  2. textbox.Clear()
  3. t1 := New(3,3,70,4, text)
  4. go t1.Play()
  5. t2 := textbox.New(3,8,70,4) // キー入力エコー用テキストボックス
  6. t3 := textbox.New(3,13,70,4) // メッセージ表示用テキストボックス
  7. keyin := bufio.NewReader(os.Stdin)
  8. for i:=0; i < len(text)+1; i++ {
  9. c,_:= keyin.ReadByte()
  10. if c == '\n' {
  11. t2.Puts("[Enter]")
  12. } else {
  13. t2.Putc(c)
  14. }
  15. if c == '\n' && i == len(text) {
  16. t3.Puts("おめでとう! 成功です.")
  17. break
  18. } else if i < len(text) && c != text[i] || c != '\n' && i == len(text) {
  19. // 途中で間違えた || 最後に[Enter]以外のキーを押した
  20. t3.Puts("残念でした.キーが違います.")
  21. break
  22. }
  23. }
  24. }

解答例(3)

  1. package main
  2. import (
  3. "time"
  4. "bufio"
  5. "os"
  6. "./textbox"
  7. )
  8. const text = "Go言語は、Googleが開発した新しいプログラミング言語です。"
  9. // --- ProblemBox ---
  10. // 問題文を0.5秒ごとに1文字ずつ表示するデータ型.
  11. type ProblemBox struct {
  12. *textbox.TextBox
  13. text []rune //[変更] string型 → []rune型(UTF8エンコードのユニコードを格納)
  14. }
  15. // 位置,大きさ,表示する問題文を指定してProblemBoxを生成する.
  16. func New(x,y,w,h int, s string) (pb *ProblemBox) {
  17. return &ProblemBox{textbox.New(x,y,w,h),[]rune(s)} //[変更] string型を[]rune型に変換
  18. }
  19. // 問題文を0.5秒ごとに表示する.
  20. func (pb *ProblemBox) Play() {
  21. pb.Clear()
  22. for i:= 0; i < len(pb.text); i++ {
  23. pb.PutRune(pb.text[i])
  24. time.Sleep(5e8)
  25. }
  26. pb.Puts("[Enter]")
  27. }
  28. // --- ProblemBox end ---
  29. func main() {
  30. textbox.Clear()
  31. textslice := []rune(text) //[追加] 問題文の型を変換
  32. t1 := New(3,3,70,4, text)
  33. go t1.Play()
  34. t2 := textbox.New(3,8,70,4)
  35. t3 := textbox.New(3,13,70,4)
  36. keyin := bufio.NewReader(os.Stdin)
  37. for i:=0; i < len(textslice)+1; i++ {
  38. c,_,_:= keyin.ReadRune(); //[変更] ReadByte()の代わりにReadRune()を使用
  39. if c == '\n' {
  40. t2.Puts("[Enter]")
  41. } else {
  42. t2.PutRune(c) //[変更] Putc()の代わりにPutRune()を使用
  43. }
  44. if c == '\n' && i == len(textslice) { //[変更] text → textslice
  45. t3.Puts("おめでとう! 成功です.")
  46. break
  47. } else if i < len(textslice) && c != textslice[i] || c != '\n' && i == len(textslice) { //[変更] text → textslice
  48. // 途中で間違えた || 最後に[Enter]以外の文字を押した
  49. t3.Puts("残念でした.キーが違います.")
  50. break
  51. }
  52. }
  53. }

解説(3)

解答例(1)(2)の文字と文字列を扱う部分をASCII以外の文字も扱えるように変更します.解答例(1)(2)からの変更部分は,コメントとしてソースプログラム中に記載します.

Go言語における文字と文字列

  • Go言語ソースの文字コードは,UTF-8でエンコードされたUnicode文字です.(ちなみに,変数名や関数名に漢字やひらがなを使うこともできます)
  • 文字リテラルは文字をクォートでくくって記述します.文字リテラルは整数定数です.UTF-8エンコードされた複数のバイト列もひとつの整数値で表します.例えば'日という文字'は,UTF-8エンコードでは0xe6,0x97,0xa5の3バイトからなりますが,0x65e5という整数値を表します.
  • 文字列リテラルは文字をダブルクォートまたはバッククォートでくくって記述します.プログラムでは10行目.
  • 文字列型は文字列の値を表現します.文字列はbyte(uint8の別名)の配列のように振舞います(ただし値は不変です).従って,文字列sのi番目のバイトはs[i]で得ることができます.
  • スライスの解説で述べたように,文字列は,byteのスライスまたはintのスライスに変換できます.
  • 文字列の値を []byte型へ変換することで,文字列のバイトデータを要素として持つスライスが得られます.例えば,[]byte("日本語")は,[]byte{0xe6, 0x97, 0xa5, 0xe6, 0x9c, 0xac, 0xe8, 0xaa, 0x9e}となります.
  • また,文字列の値を[]int型へ変換することで,文字列の各Unicodeコードポイントを持つスライスが得られます.例えば,[]int("日本語")は,[]int{0x65e5, 0x672c, 0x8a9e}となります.プログラムの40行目で文字列型の変数textを整数のスライスtextsliceに変換し,以降ではtextsliceを処理の対象としています.49行目,56行目,59行目.

ReadRune(),PutRune()

  • 解答例(1)(2)では,キーボード入力をバイト単位で読み込むためにbufioパッケージのReadByte()メソッドを利用していました.解答例(3)では,バイト単位ではなく文字単位で読み込むためにReadByte()の代わりにReadRune()を利用しています(50行目).ReadRune()は,Reader型から一文字読み込み,その読み込んだ文字とバイト数とエラーコードを返します.読み込んだバイト数は使わないので,'_'で無視しています.エラーコードも無視しています(こちらは使わないのでではなく,手抜きです ^_^;).
  • 29行目と42行目で,ProblemBoxやTextBoxに文字を出力するのに,PutRune()というメソッドを使っています.これはtextboxパッケージで定義したメソッドです.文字(整数値)を引数にとり,それをProblemBoxに表示します.