C# 7 の Tuple は戻り値が複数の時に便利
2017/3/11、「Visual Studio 2017 リリース記念勉強会 & まどすた #2」に参加してきました。
1コマ目のセッションとして、岩永 信之(@ufcpp)さんの「C# 7」セッションを聞かせていただきました。
C# 7の新機能について非常に細かく説明していただきましたが、その中で Tuple についてフォーカスしてブログっておきます。
※以下は、セッションでの解説内容ではなく、それを受けて私が調査した内容になります。
Tuple は戻り値が複数の時に便利
ということで、C# 7 で使い勝手がよくなった Tuple ですが、複数の戻り値があるメソッドで使うのが最も美しい使い方だと思いました。
(Tupleは乱用すると見通しの悪い、属人 巧 のコードが生成される可能性があると思っているので)
では、以下のCompanyクラスをベースに説明を進めます。
// 会社クラス public class Company { ... 正社員数とパートナー社員数を取得するメソッドを追加していくよ! }
2つの戻り値を持つ同期メソッドの場合
Companyクラスに以下のメソッドを追加します。
[リスト1] // Companyクラスに追加するメソッド // outパラメータを使用して2つの戻り値を返す同期メソッド static public void GetEmployeeCountSync( out int properCount, out int partnerCount) { properCount = 100; partnerCount = 30; }
// リスト1の呼び出し側 Company.GetEmployeeCountSync(out int properCount, out int partnerCount); Console.WriteLine("proper:{0} partner:{1}", properCount, partnerCount); // 出力結果:proper:100 partner:30
2つの戻り値があるので、outパラメータを使うというよくある実装です。
※呼び出し側実装における、outパラメータを事前宣言せず、メソッド呼び出し時に宣言する手法はC# 7の新機能です。
次にリスト1を Tuple戻り値 を使って書き換えます。
[リスト2] // Companyクラスに追加するメソッド // Tupleを使用して2つの戻り値を返す同期メソッド static public (int properCount, int partnerCount) GetEmployeeCountSyncTuple() { int properCount = 100; int partnerCount = 30; return (properCount, partnerCount); }
// リスト2の呼び出し側 (int properCount, int partnerCount) = Company.GetEmployeeCountSyncTuple(); Console.WriteLine("proper:{0} partner:{1}", properCount, partnerCount); // 出力結果:proper:100 partner:30
または、以下のように呼び出すこともできます。
// リスト2の呼び出し側 var result = Company.GetEmployeeCountSyncTuple(); Console.WriteLine("proper:{0} partner:{1}", result.properCount, result.partnerCount); // 出力結果:proper:100 partner:30
戻り値の Tupleオブジェクト を分割して各々の変数に受け取るか、まとめて受け取るかの違いになります。
2つの戻り値を持つ非同期メソッドの場合
この非同期メソッドのケースで Tuple戻り値 の威力がより発揮されると思います。
非同期メソッド、つまり asyncキーワードの付いたメソッドでは、「引数に out / ref キーワードをつけることができません」。これは言語仕様としてそのようになっています。
[リスト3] // これはコンパイルエラー static public async Task GetEmployeeCountAsync( out int properCount, // asyncなのにoutはNG out int partnerCount) { properCount = 100; partnerCount = 30; return; }
その為、複数の戻り値を持つ必要がある場合、それらを集約したクラスを定義する必要があります。
例えば以下のような感じ。
[リスト4] public class CountResult { public int ProperCount { get; set; } public int PartnerCount { get; set; } } ... static public async Task<CountResult> GetEmployeeCountAsync() { CountResult result = new CountResult() { properCount = 100, partnerCount = 30 }; // ... return result; }
しかし、Tupleを利用すれば同期メソッドと同様の実装方式をとることができます。
[リスト5] // Companyクラスに追加するメソッド // Tupleを使用して2つの戻り値を返す非同期メソッド static public async Task<(int properCount, int partnerCount)> GetEmployeeCountAsyncTuple() { int properCount = 100; int partnerCount = 30; await Task.Delay(1000); return (properCount, partnerCount); }
// リスト5の呼び出し側 (int properCount, int partnerCount) = Company.GetEmployeeCountAsyncTuple().Result; Console.WriteLine("proper:{0} partner:{1}", properCount, partnerCount); // 出力結果:proper:100 partner:30
戻り値がオブジェクト型の場合
前述の例では戻り値の型がリテラル型でした。
オブジェクト型の場合は、非同期メソッドにおいても引数渡しで結果を受け取ることが出来ます(C# 7に関係なく従来の言語仕様的に)。
その部分の振る舞いを見てみたいと思います。
[リスト6] static public async Task GetEmployeesAsync( List<Employee> properEmployees, List<Employee> partnerEmployees) { for (int i = 0; i < 10; i++) { // 1秒毎にEmployeeを追加 await Task.Delay(1000); properEmployees.Add( new Employee() { Name = "proper" + i.ToString() }); } for (int i = 0; i < 5; i++) { // 1秒毎にEmployeeを追加 await Task.Delay(1000); partnerEmployees.Add( new Employee() { Name = "partner" + i.ToString() }); } return; }
// リスト6の呼び出し側 List<Employee> properEmployees = new List<Employee>(); List<Employee> partnerEmployees = new List<Employee>(); var task = Company.GetEmployeesAsync(properEmployees, partnerEmployees); while (true) { Console.WriteLine("{0} proper:{1} partner{2}", DateTime.Now.ToString("H:m:s"), properEmployees.Count, partnerEmployees.Count); System.Threading.Thread.Sleep(1000); if (task.IsCompleted) // Taskの終了を確認 break; } // 出力結果:proper:100 partner:30 17:21:5 proper:0 partner0 17:21:6 proper:1 partner0 17:21:7 proper:2 partner0 17:21:8 proper:3 partner0 17:21:9 proper:4 partner0 17:21:10 proper:5 partner0 17:21:11 proper:6 partner0 17:21:12 proper:7 partner0 17:21:13 proper:8 partner0 17:21:14 proper:9 partner0 17:21:15 proper:10 partner0 17:21:16 proper:10 partner1 17:21:17 proper:10 partner2 17:21:18 proper:10 partner3 17:21:19 proper:10 partner4
非同期メソッド GetEmployeesAsync() を呼出し後、whileループで引き渡したEmployeeオブジェクトを監視&コンソール出力しています。
properEmployees.Count / partnerEmployees.Countが順次カウントアップされているのが確認できます。
GetEmployeesAsync()は1秒毎にEmployeeオブジェクトを追加し、呼び出し元は1秒毎にEmployeeリストオブジェクトの内容を確認しています。
つまり、同一のオブジェクトを別スレッドで互いに参照している状態になっています。これについては、プログラム上のデッドロック等が発生しないように考慮が必要なケースがあるでしょう。
リスト6の実装をTuple戻り値版に書き換えたものが以下になります。
[リスト7] // リスト6をTuple戻り値版に書き換え static public async Task<(List<Employee> properEmployees, List<Employee> partnerEmployees)> GetEmployeesAsyncTuple() { List<Employee> properEmployees = new List<Employee>(); List< Employee > partnerEmployees = new List<Employee>(); for (int i = 0; i < 10; i++) { await Task.Delay(1000); properEmployees.Add(new Employee() { Name = "proper" + i.ToString() }); } for (int i = 0; i < 5; i++) { await Task.Delay(1000); partnerEmployees.Add(new Employee() { Name = "partner" + i.ToString() }); } return (properEmployees , partnerEmployees); }
// リスト7の呼び出し側
var (properEmployees, partnerEmployees) = Company.GetEmployeesAsyncTuple().Result;
もしくは
// リスト7の呼び出し側
var task = Company.GetEmployeesAsyncTuple();
(properEmployees, partnerEmployees) = task.Result;
いずれにしても、2つの戻り値(結果)オブジェクトは、GetEmployeesAsyncTuple()非同期メソッド処理が完了してから得られる(処理中には対象オブジェクトへの参照が得られない)為、対象オブジェクトへのスレッドセーフが保証されます。
所感
本投稿の Tuple に限らず、C# 7は「言語仕様としての成熟期に入った」という印象です。
内部関数だったり、「=>」の強化による短縮記述だったり、より洗練された簡潔なコードの記述が可能になっています。
と、同時に思うことは、開発メンバーが非精鋭なチームの場合、洗練された簡潔なコードよりも「冗長だけど昔の技術知識で読み解けるコード」っていうのも重宝されるのかなぁ・・・なんていうネガティブな思いです(笑)。
でも、まあ、LINQなんかもそうだけど、しばらく時が経てば標準的に使われるようになるのかな、とも思います。
参考URL
MS公式 C# 7 の新機能紹介は以下
blogs.msdn.microsoft.com
「2017/3/11 Visual Studio 2017 リリース記念勉強会」岩永さんセッションのスライドは以下
docs.com