佐々木屋

技術的なことから趣味まで色々書きます

電卓アプリを作ろう(⑤各演算クラス)

各演算クラスを作ります。

演算管理クラスでdelCalcデリゲートに保持する必要がありますので、以下の条件を満たさなければなりません。
・引数はデリゲートするメソッドの引数(数、型両方)と同一でなければならない
・戻り値はデリゲートするメソッドの戻り値と同一でなければならない



演算は数値が2つ必要です。1つは演算子、もう1つは演算子です。
は演算デリゲートを作成する際に設定しますので、フィールド変数にコンストラクタを利用して設定するとよいでしょう。

は演算デリゲートでメソッドを実行する際に利用しますので、メソッド引数として渡すようにします。


以上で基本的な電卓アプリのクラス説明は終了です。これらを参考にオブジェクト指向型の電卓を作ってみましょう。手続き型の電卓と考え方、構築の仕方が違うことに気が付くはず?です。


回答例は少し時間をおいて掲載します。まずは自力でやってみましょう。

電卓アプリを作ろう(④フォームクラス)

ボタン押下時の振る舞いを行うクラスです。
こんな感じで適当にコントロールGUIで作ります。各ボタンとテキストボックスを用意します。なお、テキストボックスは桁数制限を設けますので、MaxLengthを20くらいにしておきましょう。簡単ですね。
f:id:sasaki816:20190127132109j:plain
ボタンは大きく分けて、演算ボタン、クリアボタン、イコールボタン、数値ボタンの4つに分類されます。それぞれのボタンの挙動をまとめておきます。

Tagプロパティ

C#VB.NETどちらともコントロール配列の機能はありません。但し、共通処理が多くなりますので、Tagプロパティを設定してどのボタンにどの役割をさせるのかを設定します。
数値ボタンは数字、演算ボタンは予め決めた演算固定値クラスを動的に設定します。なお、この部分は別の機会に説明しますので、よく分からない場合は直接数値を入力してもかまいません。

演算ボタン

四則演算が押下されたときの振る舞いです。演算管理クラスへ何のボタンが押下されたかをプロパティで渡します。
電卓の機能として、
 ①通常の演算キー押下
 ②演算キーの連続押下(押し直し)
 ③=押下後の演算キー押下
の3パターンを想定する必要があります。ただ実際は①と②は処理として等価にしてしまえば、2通りの想定で事足ります。

①②の場合は単純に演算管理クラスのEntryOpeプロパティに押下されたTagの値を渡します。

③の場合は少しややこしいです。イコールが押下されていますので、現在表示されている結果を基に演算キーを演算管理クラスに渡す必要があります。ここで、「現在表示されている結果を基に」の部分をどうしたらよいかをオブジェクト指向っぽく考えてみて下さい。

クリアボタン

演算全体をクリアするキー「C」と入力値のみをクリアするキー「CE」と2種類必要です。「CE」は表示が「0」になればいいので、演算管理クラスのShowNumプロパティを全て書き換える必要があります。ChangeNumプロパティを使用しましょう。

「C」はすべてクリアするわけですので、電卓アプリ起動時と同じ状況にすれば良いということになります。

数値ボタン

演算管理クラスへ数値を渡します。こちらも=押下後の処理が別途必要です。

イコールボタン

演算管理クラスへイコールが押下されたことを通知します。


以上を参考に作ってみましょう。

TryParseメソッドのカンマ区切り挙動

TryParseメソッドは、数値ではなくカンマ区切りの数値を渡したときの挙動が型によって微妙に違います。

よく使われるint、long、double、decimalで説明します。

int value;
Console.WriteLine(int.TryParse("1,234", out value)); //false

long value;
Console.WriteLine(long.TryParse("1,234", out value)); //false

double value;
Console.WriteLine(double.TryParse("1,234", out value)); //true

decimal value;
Console.WriteLine(decimal.TryParse("1,234", out value)); //true
Dim value As Integer
Console.WriteLine(Integer.TryParse("1,234", value)) 'False

Dim value As Long
Console.WriteLine(Long.TryParse("1,234", value)) 'False

Dim value As Double
Console.WriteLine(Double.TryParse("1,234", value)) 'True

Dim value As Decimal
Console.WriteLine(Decimal.TryParse("1,234", value)) 'True

double型とdecimal型は問題なく型変換できますが、int型とlong型ではエラーになります。なぜこのようなことが起きるのでしょうか。


原因はSystem.GlobalizationクラスのNumberStyles列挙体の既定値が異なるためです。double型とdecimal型はAnyが指定されていますが、int型とlong型は(恐らく)Integerが指定されています。この為カンマが入った数値は許容外となるためfalseを返すわけです。

これを回避するには、NumberStyles列挙体でAllowThousandsなどを明示的に指定します。なお、NumberStyles列挙体を第二引数に指定した場合、第三引数にNumberFormatInfoクラスを指定しなければなりません。通常は現在のカルチャに対して実行しますので、CurrentInfoを指定します。

int value;
Console.WriteLine(int.TryParse(" 1234", NumberStyles.AllowThousands
                               , NumberFormatInfo.CurrentInfo, out value));
Dim value As Integer
Console.WriteLine(Integer.TryParse("1,234", NumberStyles.AllowThousands _
                                   , NumberFormatInfo.CurrentInfo, value))



参考までにNumberStyles列挙体の一覧をまとめておきます。

フィールド 許容内容
AllowCurrencySymbol 通貨記号
AllowDecimalPoint 小数点
AllowExponent 指数
AllowHexSpecifier 16進数
AllowLeadingSign 先頭符号
AllowTrailingSign 後続符号
AllowLeadingWhite 先頭空白
AllowTrailingWhite 後続空白
AllowThousands 3桁区切りカンマ
Any 16進数(AllowHexSpecifier)以外全て
Currency 指数(AllowExponent)と16進数(AllowHexSpecifier)以外全て
Float 先頭空白(AllowLeadingWhite)、最後空白(AllowTrailingWhite)
指数(AllowExponent)、小数点(AllowDecimalPoint)
先頭符号(AllowLeadingSign)
HexNumber 先頭空白(AllowLeadingWhite)、最後空白(AllowTrailingWhite)
16進数(AllowHexSpecifier )
Integer 先頭空白(AllowLeadingWhite)、最後空白(AllowTrailingWhite)
先頭符号(AllowLeadingSign)
None なし
Number 先頭空白(AllowLeadingWhite)、最後空白(AllowTrailingWhite)
先頭符号(AllowLeadingSign)、後続符号(AllowTrailingSign)
小数点(AllowDecimalPoint)、3桁区切りカンマ(AllowThousands)

電卓アプリを作ろう(③演算管理クラス)

早速クラスを作っていくわけですが、演算管理クラスがメインのクラスになるので、まずそこから構築していきます。関係性をおさらいしておきましょう。
f:id:sasaki816:20190208144837j:plain
今回は一番大きい演算管理クラスを作成します。

メンバー

以下が必要なメンバー構成です。なお、各メンバーの名前は自分で分かりやすい名前に変更しても構いません。

名前 種類 詳細内容
ErrorFlg プロパティ bool 計算結果のエラー判断。
EqualFlg プロパティ bool イコールボタンの押下を保持。
num フィールド decimal 入力された数値をEntryNumプロパティから設定。計算に使用する。
ShowNum プロパティ string テキストボックスに表示する内容を保持。
入力された数値はこのプロパティの文字列に追加する形。
EntryNum プロパティ
(setterのみ)
string 入力された数値をShowNumに追加、その結果から計算用numに保存する。
ChangeNum プロパティ
(setterのみ)
decimal ShowNumプロパティの値を全て書き換えて、計算用numに保存する。CEキー押下等で使用する。
ope フィールド int 入力された演算をEntryOpeプロパティより設定。
opeFlg フィールド bool EntryOpeが実行されたときに設定。
EntryOpe プロパティ
(setterのみ)
string 入力された演算をopeに保持、opeFlgも設定。
delCalc デリゲート 各演算クラス
計算メソッド
入力された数値と演算を演算クラスに渡してデリゲートを作る。
Calculate() メソッド void
VB.NETはSub)
演算管理メソッド。詳細は後述。
KetaCheck(decimal) メソッド string 桁チェック。今回は15桁で設定する。

数値入力プロパティ EntryNum

演算自体が実行される条件は、数値が押下されたとき、既に演算キーが押下されているかどうか(opeFlg)です。opeFlg = true の場合は演算が実行されます。例えば4+5の場合を考えると、
 4+
の時点では、プロパティにより各フィールドに保持されているだけに過ぎず、
 4+5
演算キー押下直後の数値「5」が入力された時、初めて「+」までの演算が実行されます。詳細を書くと、
 0+4+
の赤字部分の演算が実行されます。次の青字部分「5」は連続する可能性があるので入力待機となります。
フローチャートにすると以下のようになります。
f:id:sasaki816:20190208173937j:plain

演算管理メソッド Calculate()

演算管理メソッドは以下のような挙動を考えます。
①ErrorFlgで操作可不可の判断
②演算デリゲートを実行
③結果の桁数チェック
④結果を表示用フィールドShowNumへ代入
⑤演算フィールドopeと結果を元に次の演算デリゲートを作成

先ほどの4+5を考えると、
 num = 4
 ope = 加算
ということで、0+4の演算が実行(②)され、④に「4」が代入、結果の値「4」で加算メソッドのデリゲートが作成されます。次に数字が5で終わる場合は、4+5の演算が実行され・・・といった具合で、あとはその繰り返しです。

フローチャートにすると以下の通りです。
f:id:sasaki816:20190213224401j:plain


デリゲートの宣言に慣れていない場合は、以下を参照して下さい。
sasaki816.hatenablog.com

読書に対する勘違い

しばらくはてなブログが重いなぁと思っていたら、やっぱりDBの不調だったようですね。業種は違えど同じSEとして、大変やったろうなぁと思います。お疲れさまでした。
maintenance.hatena.ne.jp


さて、私の勤務している会社でしばらく前に流行っていた(言い方悪いけど)風習で、参考文献を強制的に買わせてレポートを書くという業務がありました。なんでも業務に役立つ?から読め、と役員が言っているようです。

結論から言えば、個人的にこれは絶対やってはいけないと思っています。
何故なら読むのも時間の無駄、書くのも時間の無駄、本を買うのもお金の無駄、
全てにおいて無駄
でしかないからです。


例えば、私がこのブログで、「整数論が何かの役に立つので買って読みましょう」と唱えてみます。でも、恐らく買う人は誰もいないでしょう。それは何故でしょうか。
books.rakuten.co.jp
理由は明確で、その本が本当に自分にとってためになるのかどうかが不明瞭だからです。


少し大袈裟な話になってしまいましたが、私は本は自分の為に読むもので、決して他人の為に読むものではなく、ましてや「読め」と強要するものでもないと思います。実際自分で本を手にして、立ち読みしてみて、心に入ってくる(自分の理解を補填してくれる)ような本と出合った時に初めて意味があるものなのです(と言いつつ、たまにジャケ買いしてしまうのはご愛敬)。

他人の良本が自分の良本になるとも限らないし、あなたの本への理解が私の本への理解につながるとも言い難いです。
ましてレポートを書くなど・・・。一体何様のつもりなんでしょうね。



但し、一つだけこれをやっても(読書を強要してレポート提出)いい場合があります。

それは自分の書いた本を読んでのレポート提出です。本は書いた人が一番内容を理解しています。その理解にどれだけ近寄れたかを知る上では最も効果的だからです。




さて、私も本を読みますが、意外や意外?年間購読数は結構少ないです。読書量としては人生の中で恐らく大学時代が一番多く、専門書も含め年間100冊以上は読んでいましたが、現在は年間7~8冊程度。約1/10以下ですね。

ただ、当時と違うのが読み返しの回数です。最低10周ぐらいします。これは読む本の性格にもよりますが、現在読んでいる本が実は過去に読んだ本につながっている!みたいな事が多いからです。


さて、今年はどんな本との出会いがあるかな?

デリゲート(基本)

デリゲートを本やサイトを調べると、「委譲」であったり、「代表者」、はたまた「C言語の関数ポインタ」という難しい表現が出てきます。ちょっと分かりにくいですよね。そして大抵、読み進めると何が書いてあるか分からなくなるのがデリゲートです。

実はデリゲートを完全に理解するには、デリゲート以外の要素の理解度が大きくかかわってきます。例えばコールバックとか、ラムダ式とか匿名メソッドとかマルチキャストとか。これらが初学者にとって少しハードルを高くしている原因だと私は思います。

デリゲート自体の仕組みは非常に単純なので、今回はそこに絞って少し砕いて説明してみたいと思います。

デリゲートって何なのさ?

簡単に言えば「型」です。stringやintなどと同じ「型」です。ただ、中に入るものはメソッド(への参照情報)が入ります。変数同様、メソッドを入れて保持、実行することが出来ます。

条件があります

デリゲートを作るには条件が2つあり、どちらも満たさなければなりません。
・引数はデリゲートするメソッドの引数(数、型両方)と同一でなければならない
・戻り値はデリゲートするメソッドの戻り値と同一でなければならない


実際につかってみよう

以下のようなクラスのメソッドをデリゲートしてみます。

public class Test1 {
    public void Hoge(string naiyo) {
        Console.WriteLine(naiyo);
    }
}
public class Test2 {
    public void Piyo(string naiyo) {
        Console.WriteLine(naiyo+ naiyo);
    }
}
Public Class Test1
    Public Sub Hoge(ByVal naiyo As String)
        Console.WriteLine(naiyo)
    End Sub
End Class
Public Class Test2
    Public Sub Piyo(ByVal naiyo As String)
        Console.WriteLine(naiyo + naiyo)
    End Sub
End Class

Test1クラスもTest2クラスもどちらにもvoid(Subプロシージャ)で引数string型一つのメソッドがあります。デリゲートする条件である同一引数、同一戻り値(今回は無し)は満たされていますので、どちらのメソッドも一つのデリゲートにすることができます。

//デリゲートの宣言
delegate void ShowDelegate (string value);

//ShowDelegate デリゲートの作成と実行
ShowDelegate dlgate = new Test1().Hoge;
dlgate("fuga");

dlgate = new Test2().Piyo;
dlgate("fuga");
'デリゲートの宣言
Delegate Sub ShowDelegate (ByVal value As String)

'ShowDelegate デリゲートの作成と実行
Dim dlgate As ShowDelegate = AddressOf New Test1().Hoge
dlgate("fuga")

dlgate = AddressOf New Test2().Piyo
dlgate("fuga")
fugafuga
fuga

最初にdelegateキーワードで宣言するのはC#VB.NETも一緒です。

次にnewキーワードを使ってメソッドの参照先のインスタンスを作ってそれを渡すわけですが、C#は感覚的にそのまま書けるのに対し、VB.NETはAddressOf演算子で指定する必要があります(正直この面倒臭さがVB.NETに嫌悪感を抱く人が多い原因でないかと思います)。

メソッドの参照先を渡すと、dlgate は対象となるクラスのメソッドとしての振る舞いを実行できます。よって、最初のdlgate("fuga")はTest1クラスのHogeメソッドが実行され、2番目のdlgate("fuga")はTest2クラスのPiyoメソッドが呼び出されます。

非同期処理、マルチスレッド(async/await)

非同期処理最終話です。

.Net Framework4.0でTaskクラスが登場し、非同期処理がより簡単に書けるようになりましたが、.Net Framework4.5からTaskクラスをもっと使いやすくしたasync/awaitが登場します。
非同期処理はThreadから始まり、Timerスレッドやデリゲート、ThreadPool等色々やり方を解説してきましたが、現時点でasync/awaitが最も主流で有用な書き方になります。

例として、以下の同期処理を考えます。
単純に6秒間待つような処理です。

private void button1_Click(object sender, EventArgs e) {
    Thread.Sleep(6000);
    Console.WriteLine("完了");
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Thread.Sleep(6000)
    Console.WriteLine("完了")
End Sub

この処理をasync/awaitを使った非同期処理に変更していきます。
簡単ですが、先にasync/awaitの機能の説明をしておきます。

async修飾子

メソッドの頭にasync修飾子をつけると非同期メソッドになります。非同期メソッドは通常のメソッドと異なり、awaitキーワードが使用できるようになります。

awaitキーワード

awaitキーワードで指定された処理は非同期で行われます。awaitキーワードで指定された処理の後は非同期処理が完了した後で処理されますので、非同期処理を安全に行うことが可能です。また、処理の結果を取り出すことも可能です。


これだけです。たったこれだけで今まで煩わしかったスレッドの管理やコールバックの利用などが全て不要となります。

非同期処理を書いてみよう

さて、例のコードを非同期メソッドを利用して変更してみます。

private async void button1_Click(object sender, EventArgs e) {
    await Task.Run(()=> Thread.Sleep(6000));
    Console.WriteLine("完了");
}
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Await Task.Run(Sub()
                     Thread.Sleep(6000)
                   End Sub)
    Console.WriteLine("完了")
End Sub

どうでしょうか。最初の例のコードに比べて、awaitのTask.Runメソッドが加わっただけで、構成としてほとんど変わっていないのです。このようにasync/awaitを利用した非同期処理の一番のメリットは、同期処理と同じ流れで非同期処理を書くことができ、様々なところでコストがかからないことです。


もう一つ例をあげてみましょう。
ボタンを押下すると、HeavyProcメソッドに何か適当な文字列を渡して6秒間スリープさせ、結果を受け取るといった単純な処理を考えます。
ただ結果を受け取るだけではつまらないので、適当にテキストボックスを作ってその入力内容をHeavyProcメソッドに渡します。

同期処理では当然ボタンを押下した瞬間に固まったようになりますので、その処理が終わらない限り次の入力は出来なくなります。

private string HeavyProc(string naiyo) {
    for (int i = 0; i <= 5; ++i) {
        Thread.Sleep(1000));
        Console.WriteLine(naiyo);
    }
    return naiyo + ":完了";
}
private void button1_Click(object sender, EventArgs e) {
    string res = HeavyProcAsync(textBox1.Text);
    Console.WriteLine(res);
}
Public Function HeavyProc(ByVal naiyo As String) As String
    For i As Integer = 0 To 5
        Thread.Sleep(1000)
        Console.WriteLine(naiyo)
    Next
    Return naiyo & ":完了"
End Function
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim res As String = HeavyProc(TextBox1.Text)
    Console.WriteLine(res)
End Sub



早速async/awaitを付けて非同期処理にしてみます。
なお、慣例として非同期メソッドはメソッド名の最後に「Async」を付けます。ただこれを付けなかったからといって動作が変わるとかはありません。あくまで慣例です。

public async Task<string> HeavyProcAsync(string naiyo) {
    for (int i = 0; i <= 5; ++i) {
        await Task.Run (()=>Thread.Sleep(1000));
        Console.WriteLine(naiyo);
    }
    return naiyo + ":完了";
}
private async void button1_Click(object sender, EventArgs e) {
    string res = await HeavyProcAsync(textBox1.Text);
    Console.WriteLine(res);
}
Public Async Function HeavyProcAsync(ByVal naiyo As String) As Task(Of String)
    For i As Integer = 0 To 5
        Await Task.Run(Sub()
                         Thread.Sleep(6000)
                       End Sub)
        Console.WriteLine(naiyo)
    Next
    Return naiyo & ":完了"
End Function
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    Dim res As String = Await HeavyProcAsync(TextBox1.Text)
    Console.WriteLine(res)
End Sub

結果はどうなりましたか?


これで非同期処理・マルチスレッドの話は一旦完了となります。

非同期処理、マルチスレッドは個人的に非常に好きで、楽しくまとめることができましたが、少々「クセ」があるので難しかったかもしれません。少しでもこの面白さが伝われば幸いです。