yumeの日記: C#学習 16
コメントありがとうございます。
非同期処理の続き
仮のプログラムを書いて、非同期処理でメッセージを表示中に、キー入力がされたらメッセージを即全部表示、というプログラムを書いてみよう。
と思ったところでいまさらな疑問点がわりと浮かぶ。
Applicationクラス、Gameクラス、Inputクラス、Outputクラスを作ったとして
・ApplicationクラスはGameクラスをmyGameインスタンスとして持つ。そしてMain関数を実行する。
・Inputに基づいてspeechSpeedを変化させたい
・OutputクラスはspeechSpeedに基づいて、テキストを表示する速度を決めたい
という場合、speechSpeed変数は誰が持つべきなんだろう?
直感的にはGameが持つべき変数だという気がする……。
しかし、そうする場合、InputクラスやOutputクラスの中でmyGame.speechSpeedにアクセスする方法として正しいやり方とは、どういったものだろう?
直感的には、speechSpeedをpublicな変数にしておいて、myGame.speechSpeed = 10;などとすればいいように思えるが、
そもそも、InputクラスやOutputクラスは「myGame」を持っていないし、それにアクセスするための方法を持っていない。
某エンジニアのお仕事以外のメモ(分冊2)曰く、
public void EditspeechSpeed(Game myGame, int editSpeed)
{
myGame.speechSpeed = editSpeed;
}
のようにメソッドにすればよいそうだ。ここでGame型でmyGameを渡すことができるらしい。refじゃなくてもいいんだ?
で、speechSpeedをpublic変数にして……。
さて「{get; set;}とはなんぞや」と思って調べてみる。
Samurai Blog @遠藤貴大曰く、
・set; get;とはプロパティである
・プロパティとは、アクセサーのことである。
・メンバ変数へ外部から直接アクセスできるのは好ましくない。(publicとかがそれだね)
なぜなら、うっかり変な値に書き換えられたり、メンバ変数に絡む修正をするとき、そのメンバ変数に触れるクラスまで修正しなきゃならなくなったりするから。
・なので、メンバ変数はなるべくprivateにして、そのクラス内のメソッドで書き換えたり読んだりすればいい。これがアクセサー。
・いちいちアクセサーを書いてたらクラス内がメソッドだらけになるから、簡潔に書くためにあるのがプロパティ
>つまり、変数の数だけgetメソッドを書かずにすむように作られた仕組みか。
でも、この結果できることは「まるでpublic変数みたいに読み書きできる!」ってことであって、
結局 myGame.speechSpeed = 15;とか外から書けてしまうなら、public変数と同じ問題を抱えているような……。
teratail @raccy曰く、
・publicフィールドがある言語は欠陥言語
・C#は歴史的に負の遺産を受け継いでいる
・C#ではそういう負の遺産を使うと指摘されるようになっている。だからC#を使うと幸せになれる
>……なんのこっちゃ。
同スレッド、@Odacchi曰く、
・いろいろメリットはある、例えば値の正当性チェックを入れることができる。
>setの中でその値が不正(例えばspeechSpeedを0未満にしちゃいけない場合)だったときに、その代入を無視したりできるのか。
>ってことは、例えば入った値を最大値最小値の範囲に収めたり、入った値を増減して入れたりもできるわけね。
同スレッド、@Yamamotize曰く、
・setだけprivateにしたりできる
>つまり、読みはpublic、書きはprivateみたいにできる。
なるほど。(原理的には違うだろうけど、実際として)読み書きに拡張性のあるpublic変数のような作りにできるのかな。
さて、非同期処理の続き。
考え方としては
・ユーザーの入力を待つ(メインスレッド)
・文字テキスト読み上げをする(サブスレッド)
を同時に行って、
・ユーザーの入力があったら、読み上げを即時停止し、全文表示する
ということをする。
状態(myStat)をenumで4つ定義する
public enum GameStats
{
ready,
readingLine,
readedLine,
readedPage
}
メインスレッドではキー入力を待ち続ける。キーを入力すると、myStatを確認し:
・readingLineなら、readedLineに切り替える。
・readedLineなら、readyに切り替える。
・readedPageなら、ゲームを終了する。
サブスレッドでは、myStatを確認し:
readyなら、readingLineにして、読み上げを開始する。
読み上げ中は、常にmyStatを確認し:
・readingLine以外の状態になると、すぐにテキストを全描画する。
・そうでなくとも、最終的にはすべてのテキストが描画される。
・描画を終えたら、myStatをreadedLineに変える。
・Lineはテストでは3行あり、すべてのLineを読み終えたら、myStatをreadedPageに変える。
という形にした。以下コード
using System;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main()
{
Game myGame = new Game();
myGame.RunGame();
}
}
class Game
{
public int speechSpeed = 20;
public enum GameStats
{
ready,
readingLine,
readedLine,
readedPage
}
public GameStats myStat = GameStats.ready;
UserInput myUserInput = new UserInput();
Scene myScene = new Scene();
public void RunGame()
{
Task.Run(() => ReadText());
System.Threading.Thread.Sleep(100);
myUserInput.GetInput(this);
}
void ReadText()
{
string[] strArr = new string[3];
strArr[0] = string.Format("これはテスト{0} これはテスト{0} これはテスト{0}", "1");
strArr[1] = string.Format("これはテスト{0} これはテスト{0} これはテスト{0}", "2");
strArr[2] = string.Format("これはテスト{0} これはテスト{0} これはテスト{0}", "3");
for (int i = 0; i < 3; i++)
{
myScene.OutputLine(this, strArr[i]);
}
myStat = GameStats.readedPage;
}
}
class UserInput
{
internal void GetInput(Game myGame)
{
//キー入力を待つ。入力されたら、状態に応じて状態を変える。
Console.ReadKey();
if (myGame.myStat == Game.GameStats.readingLine)
{
myGame.myStat = Game.GameStats.readedLine;
}
else if (myGame.myStat == Game.GameStats.readedLine)
{
myGame.myStat = Game.GameStats.ready;
}
else if (myGame.myStat == Game.GameStats.readedPage)
{
Console.WriteLine("処理終了");
Console.ReadKey();
Environment.Exit(0);
}
GetInput(myGame);
}
}
class Scene
{
public int OutputLine(Game myGame, string genLine)
{
//myStatがreadyになるまで待機。
//readyなら、テキスト描画開始
//描画中、readingLineである限りウェイト入りで描画する
//そうでないなら、即座にテキストを全て描画する。
//描画を終えたら、readedLineに変更。
while (myGame.myStat != Game.GameStats.ready)
{
System.Threading.Thread.Sleep(30);
}
myGame.myStat = Game.GameStats.readingLine;
for (int i = 0; i < genLine.Length; i++)
{
if (myGame.myStat == Game.GameStats.readingLine)
{
System.Threading.Thread.Sleep(myGame.speechSpeed);
}
Console.Write(genLine[i]);
}
Console.WriteLine();
myGame.myStat = Game.GameStats.readedLine;
return 0;
}
}
}
ちょっと不安だったが動いているようなので、次はこれをConsole Adventureに反映したい。
C#学習 16 More ログイン