非同期版読み取り用メソッドを定義する
連載C#でCSVファイル操作ライブラリ の3回目の記事です。
が、以下のご指摘を頂き、追加記事を公開しております。こちらも併せてご参照ください。
非同期版読み取り用メソッドを定義する (改)
@pierusan2010 asyncにおいて「やるべきでない」点を幾つか踏んでるかもー。1. awaitせずTaskを直に返せるものはasyncにせずそのまま返したほうが良い 2. Task.Runでラップしただけのような偽Asyncメソッドは基本避ける
— neuecc (@neuecc) April 11, 2014
おさらい
前回は、XsvReaderにテキストを読み取るためのメソッドReadLine(), ReadXsvLine(), ReadXsvToEnd()
を作成しました。
今回は、これらをawait可能にした「非同期版」を作成し、XsvReaderに追加したいと思います。
async/awaitを使用した非同期メソッド
async/awaitキーワードを使用した非同期メソッドを実装します。
基本的に.Net Framework4.5以降が対象となります。
ReadLine()の非同期版ReadLineAsync()メソッド
XsvReader
の内部リーダTextReader
の非同期メソッドReadLineAsync()
をそのままラップします。
ポイントは
- 作成するメソッドに
async
キーワードを付ける - 内部リーダの
ReadLineAsync()
メソッドにawaitを付ける - 戻り値をTaskにする
public Task<string> ReadLineAsync() { return BaseReader.ReadLineAsync(); }
ReadXsvLine()の非同期版ReadXsvLineAsync()
前回作成したメソッドReadXsvLine()
を、Task.Run()
メソッドを使用して非同期処理に変換します。
Task.Run()
のFunc<TResult>
デリゲートを引数に渡すオーバーロードを使用します。このメソッドはTask<TResult>
を返します。- デリゲート内では同期メソッド
ReadXsvLine()
を実行します。
ReadXsvLine()
はIEnumerable<string>
を返すのですが、これをToArray()
で配列に展開したものを返却しています。 そうしないと、IEnumerable<T>
に包み込む部分のみが非同期に実行され、肝心のParse処理自体は、タスク完了後にforeach等でシーケンスを展開した時点で実行されて非同期にした意味が無くなってしまいます。
public async Task<string[]> ReadXsvLineAsync(ICollection<string> delimiters) { return await Task.Run(() => ReadXsvLine(delimiters).ToArray()); }
ReadXsvToEnd()の非同期版ReadXsvToEndAsync()
ReadXsvLineAsync()
の場合と同様に、同期版のReadXsvToEnd()
をTask.Run()
で非同期処理に変換します。
ReadXsvToEnd
メソッドはIEnumerable<string[]>
を返却しますが、ここではToList()
で一旦Listに展開したものを返却するようにしています。
public async Task<IList<string[]>> ReadXsvToEndAsync(ICollection<string> delimiters) { return await Task.Run(() => ReadXsvToEnd(delimiters).ToList()); }
(メモ) 戻り値でstringの配列となっている部分
Task<string[]>
、Task<IList<string[]>>
は、IList<T>
インターフェースを使ってTask<IList<string>>
、Task<IList<IList<string>>>
とする方が良かったかも?
この場合に限らず、コレクションを返却したり、プロパティで公開したりする場合のインターフェースに何を選択するかには、結構悩みます...
使用例
上記3つのメソッドを使った、簡単な例を載せておきます。
ReadLineAsync()
でcsvでない先頭行を、ReadXsvLineAsync()
でcsvのヘッダ行を、ReadXsvToEndAcync()
でヘッダ以降のすべてのレコードをそれぞれ読み取ります。
using HandyUtil.Text.Xsv; using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { ReadSample().Wait(); Console.ReadLine(); } static async Task ReadSample() { var data = @"[sample.csv@20140401] col1,col2,col3,col4,col5 one,two,three,four,five aaa,bbb,ccc,ddd,eee りんご,みかん,バナナ,ぶどう,パイナップル"; var delimiters = new[]{","}; using(var reader = new XsvReader(new StringReader(data))) { var comment = await reader.ReadLineAsync(); Console.WriteLine("comment= " + comment); var header = await reader.ReadXsvLineAsync(delimiters); Console.WriteLine("header= " + string.Join(",",header)); var rows = await reader.ReadXsvToEndAsync(delimiters); Console.WriteLine("rows= {"); foreach (var row in rows) { Console.WriteLine(" " + string.Join(",", row)); } Console.WriteLine("}"); } } } }
以下、出力結果です。
> comment= [sample.csv@20140401]
> header= col1,col2,col3,col4,col5
> rows= {
> one,two,three,four,five
> aaa,bbb,ccc,ddd,eee
> りんご,みかん,バナナ,ぶどう,パイナップル
> }
(おまけ).Net4.0の場合は?
async キーワードが無いこと以外、メソッドのシグネチャに変更ありません。
メソッド内では、Task.Run()の代わりにTask.Factory.StartNew()を使用します。
public Task<string> ReadLineAsync() { return Task.Factory.StartNew(() => BaseReader.ReadLine()); } public Task<string[]> ReadXsvLineAsync(ICollection<string> delimiters) { return Task.Factory.StartNew(() => ReadXsvLine(delimiters).ToArray()); } public Task<IList<string[]>> ReadXsvToEndAsync(ICollection<string> delimiters) { return Task.Factory.StartNew(() => (IList<string[]>)ReadXsvToEnd(delimiters).ToList()); }
使う側では、これらのメソッドをContinuWith + Unwrap
で順に繋げていく感じになりますでしょうか。
using HandyUtil.Text.Xsv; using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace ConsoleApplication2 { class Program { static void Main(string[] args) { var data = @"[sample.csv@20140401] col1,col2,col3,col4,col5 one,two,three,four,five aaa,bbb,ccc,ddd,eee りんご,みかん,バナナ,ぶどう,パイナップル"; using (var reader = new XsvReader(new StringReader(data))) { ReadSample(reader).Wait(); } Console.ReadLine(); } static Task ReadSample(XsvReader reader) { var delimiters = new[] { "," }; return reader.ReadLineAsync().ContinueWith(task => { Console.WriteLine("comment= " + task.Result); return reader.ReadXsvLineAsync(delimiters); }).Unwrap().ContinueWith(task => { Console.WriteLine("header= " + string.Join(",", task.Result)); return reader.ReadXsvToEndAsync(delimiters); }).Unwrap().ContinueWith(task => { var rows = task.Result; Console.WriteLine("rows= {"); foreach (var row in rows) { Console.WriteLine(" " + string.Join(",", row)); } Console.WriteLine("}"); }); } } }