としおの読書生活

田舎に住む社会人の読書記録を綴ります。 主に小説や新書の内容紹介と感想を書きます。 読書の他にもワイン、紅茶、パソコン関係などの趣味を詰め込んだブログにしたいです。

カテゴリ: C#

3753817_s

仕事でWPFアプリケーションの回収をしていたところなんでもシングルスレッドでやってしまっているようなコードがたくさんあり、それが原因で設計どうり動いていないコードがちらほらありました。

そこで本記事ではWPFで重い処理などを非同期処理で実装するべき理由を3つ紹介していきます。


非同期処理で実装する理由


1.画面の描画が行われない


シングルスレッドで全て実装してしまうと重たい処理が走る前に画面の表示を変更する処理をいれているにも関わらず、画面の表示が変更されるのは重たい処理が終わった後になります。

例えば以下のような画面があるとします。

キャプチャ


この画面の「重たい処理」ボタンを押せばボタン上部にStartという文字が現れ、重たい処理終了後にEndという文字が表示されるとしましょう。

このときボタン押下時の処理を以下のように同期処理で実装してしまうとStartという文字が画面には表示されません。

private void Button_Click1(object senderRoutedEventArgs e)
{
test.Content = "Start";

// 重たい処理開始
    for (int i=0i<1000000; ++i)
    {
     for (int j = 0j < 1000000; ++j)
        {
         Math.Sqrt(j);
        }
    }
// 重たい処理終了

    test.Content = "End";
}

こういう場合に重たい処理を以下のように非同期処理で実装していると開始時にはStartという文字が表示され、終了時にはEndという文字が表示されます。

private async void Button_Click1(object senderRoutedEventArgs e)
{
    test.Content = "Start";

    // 重たい処理開始
    await Task.Run(() =>
    {
        for (int i = 0i < 1000000; ++i)
        {
            for (int j = 0j < 1000000; ++j)
            {
                Math.Sqrt(j);
            }
        }
    });
    // 重たい処理終了

    test.Content = "End";
}






2.ウィンドウが固まる


重い処理を非同期処理で実装していない場合、ウィンドウが固ってしまいウィンドウを移動させることができないという問題が起こります。

重い処理をしているんだからウィンドウが動かせないぐらいいいだろうとか思う人もいるかもしれませんが、ユーザからしたら使い勝手が最悪です。

最小化ボタンとかがついていないアプリの場合、常に画面の前面にでてきてかなりうざいです。

オープンソースのアプリでもたまにそういったものがありますが、ユーザの使い勝手を考えましょう。


3.アプリがダウンする


先ほどウィンドウが固まるという話をしましたが、最悪アプリがダウンする可能性もあります。

実際に私の携わっていた仕事の一つでは5時間ほど処理時間がかかるのにその間に誤って画面をクリックするとアプリがダウンするというものもありました。

個人開発レベルでもこれは不便だと思うので非同期処理をいれて対策をしましょう。



まとめ


今回、重い処理を非同期処理でするべき代表的な理由3つを紹介しました。

他にも色々とふべんな点があるのですが、同期処理で実装するメリットは設計が楽で手抜きできるぐらいしかなくデメリットが大きいのでWPFアプリケーションを作る場合は、スレッドを意識しながら設計やコーディングをするようにしましょう。







3753817_s

WPFアプリケーションを作成していたらResizeModeをCanMinimizeにしているにも関わらず、アプリを付けたまま数時間放置していたらウィンドウが勝手に最大化するという問題が発生しました。

今回は同じ問題にはまった人のために対処法を紹介していきます。


【目次】
対処法
まとめ



ウィンドウが最大化する問題の対処法


根本原因をつかめるのが一番良かったのですが原因は分かりませんでした。

この問題が発生するPCと発生しないPCがあったりするのでWindowsの設定の問題なのかな…。

とりあえず対処法として画面が最大化したときだけ、自動的に画面を通常サイズに戻すというイベントを入れることにしました。

ウィンドウサイズが変更されたことはStateChangedを使って判断します。

サンプルコードは以下の通りです。

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        ResizeMode="CanResize"
        StateChanged="WindowStateChanged">
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
        <StackPanel>
            <Label x:Name="Label1" Margin="10" Height="40" Width="200" HorizontalContentAlignment="Center"/>
            <Button Content="ボタン" Height="40" Width="80" Click="Button_Click"/>
        </StackPanel>
    </Grid>
</Window>



using System.Windows;



namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object senderRoutedEventArgs e)
        {
            Label1.Content = "Hello World!!";
        }

        private void WindowStateChanged(object senderobject e)
        {
            if (this.WindowState == WindowState.Maximized)
            {
                this.WindowState = WindowState.Normal;
            }
        }
    }
}


簡単にサンプルコードの説明をすると画面のサイズが変わるとStateChangedイベントが走り、WindowStateChangedメソッドをコールします。

WindowStateChangedは画面の状態が最大化なら通常サイズにするという処理をしているだけです、

今回サンプルなのでResizeModeはCanResizeにしているのですが、最大化をしたくない場合はResizeModeをCanMinimizeかNoResizeにしましょう。



まとめ


今回は勝手にウィンドウが最大化する問題の応急処置的な対処法を紹介しました。

誰か根本的な原因が分かる人がいればコメントで教えてほしいです…。

画面系のアプリは色々と奥が深くて難しいですね。



3753817_s

今回はC#の非同期処理を理解するためにawaitとasyncについて勉強していきます。


asyncとawaitとは


asyncとawaitとはC#5.0以降から追加された機能です。

asyncとawaitを使うことで実装が面倒な非同期処理をシングルスレッドを書くかのような感じで実装することができます。

awaitとは文字通り待機するという意味です。

awaitを付けた場合その処理は一度別スレッドで実行されますが、その処理が終了するまでメインスレッドも待機したまま処理が進みません。

asyncはawaitを使用する際にメソッドにつけるおまじないのようなものです。

asyncとawaitを使ったメソッドは以下のようなものになります。

static async Task Main()
{
    await TaskA();
}



awaitを使って非同期処理が終わるまで待機する


それではawaitを使って非同期処理が終了するまでメインスレッドが待機する処理を実装してみます。

using System;
using System.Threading.Tasks;

class AsyncSample
{
    static async Task Main()
    {
        Console.WriteLine("Start");
        await AsyncProcess();
        Console.WriteLine("End");
    }

    private static async Task AsyncProcess()
    {
        await Task.Run(() =>
        {
            for (int i = 0i < 1000; ++i)
            {
                Console.WriteLine(i);
            }
        });
    }
}


実行結果は以下の通りです。

Start
0
1
2
3
~
999
End

実行結果から見て分かる通りAcyncProcessメソッドの処理が終了するまでMainメソッドの処理が進んでいませんね。





非同期処理を待たない


先ほどはawaitを使って非同期処理が終わるまで待機しましたが時と場合によっては、せっかく非同期処理をしているので処理が終わるのをまたずにメインスレッドを進めたい場合もあります。

そういうときはawaitを付けなければいいだけです。

先ほどのサンプルからMainメソッドを以下のように変更してみます。

static async Task Main()
{
    Console.WriteLine("Start");
    AsyncProcess();
    Console.WriteLine("End");
}

実行結果は以下の通りです。

Start
0
1
2
3
~
18
End

AcyncProcessメソッドの処理が終了するのを待たずにMainメソッドの処理が進んでいますね。

awaitを使わない場合、デッドロックにならないように注意する必要があります。

複数のスレッドで同じ変数を変更したり、参照していたりするとデッドロックが起きる可能性がありますので注意しましょう。



まとめ


今回はawaitとasyncの基本的な使用方法について学んでいきました。

awaitとasyncのおかげでC#は手軽に非同期処理が実装できるので楽ですね。

次はasyncをつけたメソッドの戻り値について勉強していきたいです。

asyncを付けたメソッドから戻り値を受け取る方法について調べました。








3753817_s

C#の非同期処理でawaitとasyncを使ったときに戻り値を受け取る方法を調べたので忘備録として記録していきます。

async/awaitの基本的な使い方は以下にまとめています。




戻り値なしの場合


戻り値なしの場合は、voidまたはTaskを戻り値として設定します。

ただ、voidを設定した場合はawaitを使って待つことはできません。

static async Task Main()
{
    Console.WriteLine("Start");
    AsyncProcess();
    Console.WriteLine("End");
}
    
private static async void AsyncProcess()
{
    await Task.Run(() =>
    {
        for (int i = 0i < 1000; ++i)
        {
            Console.WriteLine(i);
        }
    });
}

そのため非同期処理でしか呼びだないような関数の場合はTaskを使った方がよさそうです。

非同期処理でvoidを使うメリットはあまりありませんね。

static async Task Main()
{
    Console.WriteLine("Start");
    AsyncProcess();
    await AsyncProcess();
    Console.WriteLine("End");
}







戻り値を受け取りたい場合


戻り値を受け取りたい場合は、戻り値の方にTask<T>を設定します。

Tにはintやstringのように受け取りたい戻り値の方が入り、Task<int>、Task<string>のように書きます。

ただ、呼び出し元や呼び出し先では型がTask<T>であることを意識しなくても大丈夫です。

Task<int>の場合はint型を戻り値として扱うのと同じように書くことができます。

サンプルは以下の通りです。

static async Task Main()
{
    int nRet = await retInt();
    Console.WriteLine(nRet);

    string strRet = await retStr();
    Console.WriteLine(strRet);
}
    
private static async Task<intretInt()
{
    int n = 0;
    await Task.Run(() =>
    {
        for (int i = 0i < 10; ++i)
        {
            n += i;
        }
    });
    return n;
}

private static async Task<stringretStr()
{
    int n = 0;
    await Task.Run(() =>
    {
        for (int i = 0i < 10; ++i)
        {
            n += i;
        }
    });
    return n.ToString();
}

非同期処理で戻り値を受け取ることができるのはawaitで待つ時だけなので注意しましょう。

awaitを使わずに関数の値を取得したい場合はメンバ変数とかに入れるのがよさそうですね。



まとめ


今回はasync/awaitを使ったときに戻り値を受け取る方法を学びました。

戻り値の方としてTask<T>を使うことさえ忘れなければ簡単に使えますね。






3753817_s

C#でフォルダを一括削除する機能を作成していたところ、読み取り専用ファイルや別のプログラムで開いているファイルの例外処理はどうするべきか悩んだので忘備録として残しておきます。


基本的なフォルダの削除


C#でフォルダを削除するにはDirectoryクラスのDeleteメソッドを使います。

フォルダを削除する例外処理なしのサンプルコードは以下の通りです。

// このディレクトリを削除する
string path = "削除したいファイルのパス";
DirectoryInfo directoryInfo = new DirectoryInfo(path);

// このディレクトリ、そのサブディレクトリ、およびすべてのファイルを削除する
directoryInfo.Delete(true);

// ディレクトリが空の場合削除する
directoryInfo.Delete(false);





try catchで例外処理


基本的な例外処理としてまずはtry catchを使って例外処理していきます。

try catchを使った場合、削除するフォルダ内に読み取り専用のファイルがあったりした場合に必ずcatchにとびフォルダは削除されません。

try catchを使う欠点は細かい例外処理ができないことです。エラーがでたら問答無用でcatchに飛んでしまうため、色々といじりたい場合には不向きといった印象です。

サンプルコードは以下の通りです。

// このディレクトリを削除する
string path = "削除したいファイルのパス";
DirectoryInfo directoryInfo = new DirectoryInfo(path);
try{
    // このディレクトリ、そのサブディレクトリ、およびすべてのファイルを削除する
    directoryInfo.Delete(true);
}
catch{
    // エラーならここに入る
}






ファイルの属性を見て例外処理をする


先ほどのtry catchの欠点を埋める方法としてファイルの属性を見て削除するかどうか決める方法があります。

ファイルの属性を見ることで読み取り専用ファイルなら属性を変更して削除するなど細かい設定を行うことができます。

ファイルの属性はFileInfoクラスのAttributesメソッドを使うことで取得できます。

読み取り専用ファイルなら属性を変更して削除するというサンプルプログラムは以下のとおりです。

using System;
using System.IO;

class FileDel
{
    public static void Main()
    {
        try{
            string path = "C:\\Users\\black\\OneDrive\\デスクトップ\\新しいフォルダー";
            DirectoryInfo directoryInfo = new DirectoryInfo(path);
            if (!CheckDeleteDirectory(directoryInfo)){
                // エラー
                return;
            }
            // 削除可能ならフォルダを削除する
            directoryInfo.Delete(true);
        }
        catch{

        }
    }

    // ディレクトリが削除可能か確認する
    private static void CheckDeleteDirectory(DirectoryInfo directoryInfo) {
        foreach (FileInfo fileInfo in directoryInfo.GetFiles()) {
            if ((fileInfo.Attributes & System.IO.FileAttributes.ReadOnly) == System.IO.FileAttributes.ReadOnly) {
                // 読み取り専用ファイルの場合、読み取り専用属性を解除する
                fileInfo.Attributes = System.IO.FileAttributes.Normal;
            }
        }

        // 再起でサブディレクトリのファイルの属性も変更する
        foreach (DirectoryInfo dirInfo in directoryInfo.GetDirectories()) {
            CheckDeleteDirectory(dirInfo);
        }
    }
}




まとめ

今回はC#のフォルダ、ファイル削除の処理について考えていきました。

単にフォルダやファイルを削除すると言ってもファイルの属性によって処理を変えるなど色々と考えることがあっておもしろいですね。





↑このページのトップヘ