[win8] StorageFile の Open は、通常の Open/Close とちょっと違う

win8 本の執筆がてら、実験プログラムのメモ書き

古き良き時代から使われた C 言語の open/close の思考は、Windows ストア アプリの StorageFile に適用されるのかな?と思っていたの出すが、「違います」という話を少し。

■なんで違うのか?

端的に言うと、StorageFile クラスには Close メソッドがありません。ええ、C 言語の場合は open/close がワンセットになっているし、C# の場合は、StreamWriter/StreamReader でも Close しないといけないよ、というのがあるのですが、Windows ストア アプリの StorageFile クラスにはありません。Stream クラスにも Close がないし、StreamWriter クラスにも Close がありません。

なんじゃ、Flush というメソッドがあるから、それでいいじゃん、とも思っていたのですが、Flush は単にバッファのフラッシュをするだけ(ディスクに書き込みする)だけで、クローズとは違います。このあたりの落とし穴はまた後日書きますが…

■Close しないとどうなるのか?

Close メソッドがない StorageFile クラスですから、そもそもクローズを呼び出せません。
なので、ファイルが閉じられないのでは?と心配になるのですが、そのあたりは OS と StorageFile の間(実際は、Stream のところ?)でうまく調節をしているようです。

■実験コード

デザイナで、こんな画面をつくっておきます。

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <Button 
        FontSize="24"
        Click="Button_Click_1"
        Content="ファイルに保存" HorizontalAlignment="Left" Margin="40,55,0,0" VerticalAlignment="Top" Height="99" Width="197"/>
    <Button 
        FontSize="24"
        Click="Button_Click_2"
        Content="ファイルから読込" HorizontalAlignment="Left" Margin="40,159,0,0" VerticalAlignment="Top" Height="99" Width="197"/>
    <TextBox 
        Name="text1"
        FontSize="24"            
        HorizontalAlignment="Left" Margin="268,55,0,0" TextWrapping="Wrap" 
        Text="ここにサンプルの文書を表示します。" 
        VerticalAlignment="Top" Height="429" Width="611"/>
</Grid>

ボタンのイベントはこちら


private StorageFile _file;
/// <summary>
/// ファイルピッカーで保存する
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
	// FileSavePicker を用意する
	var picker = new FileSavePicker();
	picker.CommitButtonText = "データの保存先を指定する";
	picker.DefaultFileExtension = ".xml";
	picker.FileTypeChoices.Add("設定", new string[] { ".txt" });
	picker.SuggestedStartLocation = PickerLocationId.Desktop;
	picker.SuggestedFileName = "Sample.txt";
	// FileSavePicker を出して、ファイルを指定する。
	StorageFile file = await picker.PickSaveFileAsync();
	if (file == null)
	{
		// キャンセルされたときは何もしない
		return;
	}
	// ファイル名をデバッグ出力する
	Debug.WriteLine("保存先 {0}", file.Path);
	// 編集中の文章を保存する
	using (var stream = await file.OpenStreamForWriteAsync())
	{
		// サイズを0にしておく
		stream.SetLength(0);
		using (var writer = new StreamWriter(stream))
		{
			writer.Write(text1.Text);
		}
	}
	// 保存先のファイルをキープしておく
	_file = file;

}

/// <summary>
/// キープしている StorageFile を再利用する
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Button_Click_2(object sender, RoutedEventArgs e)
{
	// 保存先がないときは何もしない
	if (_file == null)
		return;

	StorageFile file = _file;
	using (var stream = await file.OpenStreamForReadAsync())
	{
		using (var reader = new StreamReader(stream))
		{
			text1.Text = reader.ReadToEnd();
		}
	}
}

文章をピッカーで保存しておいて、再び読み込むだけのプログラムです。メモ帳でありがちな「保存」ボタンの場合は、保存のほうで StorageFile を再利用するのですが、ここでは実験のために、読み込みのところで再利用をしています。

どうせクローズがないので using を使う必要もないのですが(Stream を破棄するために Dispose が必要なんですが)、まあつけておきます。Windows ストア アプリの場合、アプリ単位のサンドボックスになっている(はず)なので、多少リソースの解放を忘れても問題ないかと…乱暴ですが。

■実験1 PickSaveFileAsync 直後にファイルが作られる

if (file == null) でブレークポイントさせるとどうなるのか?ってのを試してみてください。
これ、別の実験のときに気づいたのですが、どうやら PickSaveFileAsync から戻ってきたときに「ファイルが作られます」。
このピッカーは、デスクトップに保存するようにしているので、デスクトップに「この時点」でファイルができます。
ちなみに、従来の Windows アプリ(デスクトップアプリ)の場合には、SaveFileDialog クラスを使いますが、ファイルは作られません…といいますか、SaveFileDialog は FileName プロパティで、ファイル名を取得するので、ファイル自体は作られないのです。

通常、Windows ストア アプリの場合、アプリのローカル フォルダー(LocalState フォルダー)にファイルを保存するので、さしても問題はないのですが、デスクトップとかスカイドライブとかにファイルを作成する場合には、この違いはちょっと注意が必要ですね。

■実験2 「保存」と「読み込み」の間にメモ帳で、Sample.txt を書き換えてみる

「保存」をしたときに StorageFile をキープして、「読み込み」のときに再利用しているわけですが…というか、アプリが続いている間はこういう再利用ができます。
ただし、アプリが中断したときは…と書こうと思って試してみると、中断→再開 をしても StorageFile オブジェクトはキープされていますね。これは後で調べておきましょう。

さて、
1.「保存」ボタンでテキストファイルに保存
2.メモ帳で、sample.txt を開いて内容を書き換える。
3.「読み込み」ボタンでテキストを読み込む

って手順をやると、途中の2で編集した結果が読み込めます。

なんとなく自然な感じがしますが(それはそれで「自然な感じがする」のは重要なのです)、よく考えるとちょっと不思議です。StorageFile クラスって、なにをキープしているのでしょうか?、そうなるとファイル名だけでもアクセスができるのでは?ってことですね。

■StorageFile クラスにはコンストラクタがない

実は StorageFile クラスにはコンストラクタがありません。

StorageFile class (Windows)
http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.storagefile(v=win.10).aspx

コンストラクタがないということは、なんらかの形でどこかのメソッドで作成してもらうわけけで、なんらかの「制限」がかけられている訳です。なんらかの「制限」ってのは、Windows ストア アプリが開けるファイルを OS 側(あるいはライブラリ側)が制限しているってことです。従来の SaveFileDialog クラスのようにファイル名を受け渡ししてから、StreamWriter クラスを使ってしまうと、どんなファイルでも開けるのですが、Windows ストア アプリでは、StreamWriter に渡すファイルオブジェクトは StorageFile に変更されているのです。
なるほど、そういうセキュリティなのね、という具合です。

■StreamWriter や TextWriter に Close がない理由

ここは想像ですが、ファイルに対しては非同期アクセスをするので(書き出し、読み出し自体は、同期メソッドが用意されているのですが、オープン自体が非同期なので)、クローズのタイミングを取るのはプログラム側では難しい、ってことなのかなと。
ただし、ここに落とし穴があって、C 言語タイプの open/close を想定していると、既存のファイルをオープンして書き出したときに不都合が生じるんですよね。いや、実際は不都合ではないのですが、OpenStreamForWriteAsync メソッドの仕様というところでしょうか。

	// 編集中の文章を保存する
	using (var stream = await file.OpenStreamForWriteAsync())
	{
		// サイズを0にしておく
		stream.SetLength(0);
		using (var writer = new StreamWriter(stream))
		{
			writer.Write(text1.Text);
		}
	}

なところで、ストリームのサイズを 0 に設定しておかないと、ファイルの書き出しが正常に行われません…というか、思ったように書き出せません。試しに stream.SetLength(0) のコメントアウトして、「文章の長さが縮まるように」してから保存してください。
保存したファイルを開くと、後ろに「ごみ」が残っているように見えます。
実は、OpenStreamForWriteAsync メソッドは、ランダムモードでオープンしている(たぶん)ので、ファイルが新規作成ではなくて、上書きになっているからなのです。

このあたり長くなるので、次の記事に続きます。

…と、ここまで書いて、いやいや「CreationCollisionOption」を付ければよいのでは?と気が付いた。後で修正。

カテゴリー: C#, windows 8 パーマリンク