20200209のC#に関する記事は8件です。

.NET Core 3.1 と VSCode でHello, World! する WPF アプリを作る

目的

Windows デスクトップアプリを作りたいが、Visual Studio 2019 をインストールしたくない。
Visual Studio Code でとりあえず Hello,World してみる。

環境

Windows 10 バージョン 1809
Visual Studio Code 1.42.0
.NET Core 3.1.101

空のアプリ作成

Visual Studio Code の Terminal でdotnet new wpfします。
そのままdotnet runすると空のフォームが表示されます。

フォームにコントロールを配置する

MainWindow.xaml を開きます。
Grid タグに<TextBlock x:Name="Text1"/>を追加します。

MainWindow.xaml
    <Grid>
        <TextBlock x:Name="Text1"/>
    </Grid>

コントロールに文字列をセットする

MainWindow.xaml.cs を開きます。
配置したText1に文字列"Hello, World!"をセットします。
例では中央寄せして、フォントサイズを 64 にしています。

MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Text1.Text = "Hello, World!";
            Text1.HorizontalAlignment = HorizontalAlignment.Center;
            Text1.FontSize = 64;
        }
    }

キャプチャ.PNG

終了ボタンの追加

画面にボタンコントロールを追加して、イベントを作成します。

MainWindow.xaml
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <TextBlock x:Name="Text1" Grid.Row="0" Text = "Hello, World!" FontSize = "64" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <Button x:Name="Button1" Grid.Row="1" Content="終了" Click="Button1_Click" Margin="20"/>        
    </Grid>
MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Button1_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }
    }

キャプチャ2.PNG

ダサい・・・

まとめ

.NET Core と Visual Studio Code でWindows デスクトップアプリが開発できることが分かりました。
簡単なツールを作る程度なら、Visual Studio 2019 など統合開発環境は不要だと思います。
画面レイアウトを作成するのは辛いので XAML Studio を試してみようかな。

XAML Studio
https://www.microsoft.com/ja-jp/p/xaml-studio/9ntls214tkmq

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.NET Core 3.1 と Visual Studio Code でHello, World! する WPF アプリを作る

目的

Windows デスクトップアプリを作りたいが、Visual Studio 2019 をインストールしたくない。
Visual Studio Code でとりあえず Hello,World してみる。

環境

Windows 10 バージョン 1809
Visual Studio Code 1.42.0
.NET Core 3.1.101

空のアプリ作成

Visual Studio Code の Terminal でdotnet new wpfします。
そのままdotnet runすると空のフォームが表示されます。

フォームにコントロールを配置する

MainWindow.xaml を開きます。
Grid タグに<TextBlock x:Name="Text1"/>を追加します。

MainWindow.xaml
    <Grid>
        <TextBlock x:Name="Text1"/>
    </Grid>

コントロールに文字列をセットする

MainWindow.xaml.cs を開きます。
配置したText1に文字列"Hello, World!"をセットします。
例では中央寄せして、フォントサイズを 64 にしています。

MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Text1.Text = "Hello, World!";
            Text1.HorizontalAlignment = HorizontalAlignment.Center;
            Text1.FontSize = 64;
        }
    }

キャプチャ.PNG

終了ボタンの追加

画面にボタンコントロールを追加して、イベントを作成します。

MainWindow.xaml
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <TextBlock x:Name="Text1" Grid.Row="0" Text = "Hello, World!" FontSize = "64" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <Button x:Name="Button1" Grid.Row="1" Content="終了" Click="Button1_Click" Margin="20"/>        
    </Grid>
MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Button1_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }
    }

キャプチャ2.PNG

ダサい・・・

まとめ

.NET Core と Visual Studio Code でWindows デスクトップアプリが開発できることが分かりました。
簡単なツールを作る程度なら、Visual Studio 2019 など統合開発環境は不要だと思います。
画面レイアウトを作成するのは辛いので XAML Studio を試してみようかな。

XAML Studio
https://www.microsoft.com/ja-jp/p/xaml-studio/9ntls214tkmq

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#とJavaの書き方の違いをまとめる

前置き

いつもはC#で開発をしていますが、使用するAPIにC#用のインターフェースがまだ整っていなくJavaで開発を始めてみました。
あまりJavaは触ったことがなかったため、C#でいう「xxxx」はJavaでいう「xxxx」というのをまとめてみました。
私がよく使っている予約語、実装方法が中心となっています。そのため全ての違いが網羅できているわけではありません。

C#とJavaの予約語、実装方法の違い

予約語の違い

C#、Javaでの予約語の違い、変わらない点をまとめてみました。
同じ、似ていると思った項目を横並びにして比較してみます。

差異 C# Java 備考
namespace package
using import
なし class class
: extends クラス継承
なし interface interface
なし abstract abstract
virtual なし 継承先で関数をoverride可能にする場合、C#はvirtualを付与、Javaはなし
override @Override C#は定義に含めるが、Javaはアノテーション(C#で言う属性、Attribute)
readonly final 読み取り専用フィールド
const final 定数
sealed final クラス継承禁止、Javaは関数のoverride禁止としても使用する
なし Enum Enuml 列挙型
struct なし 構造体

Javaには「struct」、「virtual」がありませんでした。
「struct」はclassでそれっぽく代用できるため問題なさそうですが、「virtual」は無いと少し困りそうです。
「virtual」については下記のタイトルでページの最後に掘り下げてみます。
・「C#のvirtualとJavaのfinal」

また、Javaの「final」がC#の複数の機能を含有しておりました。
それだけでなくJavaのみの使い方があるため、この違いについては下記タイトルでページの最後に掘り下げてみます。
・「定数の宣言、設定方法の違い」

実装方法の違い

C#での実装方法、Javaでの実装方法、どちらにしかない実装方法をまとめてみました。
こちらも同じ、似ていると思った項目を横並びにして比較してみます。

やりたいこと C# Java
属性自作 Attributeの継承 @interfaceでクラス定義
拡張メソッド public static class なし
interfaceのデフォルト実装 なし
C#8.0 (.NET Core 3.0)で追加
interfaceを継承し、メンバをdefaultで定義
リソースの自動解放 using(Stream stream = new Stream()) try-with-resources
型パラメータの制約 where T: int <T extends Number>
上記例)void Test<T>(T arg) void Test<T>(T arg) where T : int <T extends Number> void Test(T arg)
Null許容型宣言 Nullable<T>,T? Optional<T>
ラムダ式 ()=>{ } ()->{ }
コールバック引数なし Action Runnable
同上 引数1つ Action<T> Consumer<T>
同上 引数2つ Action<T1,T2> BiConsumer<T,U>(引数は2つまで)
同上 返り値あり Function<R> Supplier<T>
API仕様の関数型インターフェースを見る限りなし
同上 引数1つ、返り値あり Function<T,R> Function<T,R>
同上 引数2つ、返り値あり Function<T1,T2,R> BiFunction<T,U,R>(引数は2つまで)

今回挙げた機能については使い勝手は違えどC#、Javaの両方が同等の機能を持っていることがわかりました。

また、コメント欄でご指摘頂いている通り、C#にもinterfaceのデフォルト実装機能がC# 8.0より追加されています。
abstractとinterfaceで迷い後者で実装したけど、継承先で同じ実装を行った経験がある人はきっといるはず。私です
これは是非活用していきたいですね。

また、C#で言う「Action」、「Function」、いわゆるコールバックの使い勝手がかなり違いそうです。
「コールバック」については下記のタイトルでページの最後に掘り下げてみます。
・コールバックの使い方の違い

C#とJavaで似ているようで違う点を掘り下げる

「予約語の違い」、「実装方法の違い」で挙げた3つについて掘り下げていきます。

C#のvirtualとJavaのfinal

C#では「virtual」でoverrideを可能にし(正確には仮想メソッドとして実装)、Javaは「final」でoverrideを禁止します。
つまりJavaの関数に「final」を付けなかった場合、C#の感覚で言うと常に「virtual」での実装になります。
いつの間にかoverrideされてる!?といった状況になりたくなければ必ず「final」を付けたほうがよさそうですね。。

※追記
コメント欄でご指摘頂いているように「final」を付与しているかいないかで呼び出しコストが変わってきます。
シビアなメモリ、処理速度が求められる場合は是非付与していきましょう。

コールバックの使い方の違い

C#とJavaのコールバックの違いはざっくりとこんな感じです。
この違いを把握していればC#とJavaで混乱するとはなさそうです。

  • Javaのコールバックは引数の数に上限があり、C#は最大16個まで取れる。

  • Javaのコールバックで数値型を引数に取れないが、C#は全ての型を引数に取れる。(※1)

※1
今回記事に挙げたJavaのコールバックは参照型のみを引数に取れます。
そのためC#と違い、Javaでは数値型を引数に取ることができません。数値が設定できなくて困惑したのは私です
その代わりに数値型用のコールバックがあり下記の名前で定義されているためこちらを使います。
・「数値型」Consumer
・Obj「数値型」Consumer
・「数値型Function」
・To「数値型」Function
・To「数値型」BiFuction

定数の宣言、設定方法の違い

C#の「const」、「readonly」、Javaの「final」を定数、読み取り専用として使用する場合の違いをまとめました。

定数宣言、代入方法 const readonly final
定数フィールドでの宣言
定数フィールドへ宣言場所以外での代入 × 〇 ※1 ×
ローカル変数での宣言 ×
ローカル変数での宣言場所以外での代入 × ×
引数への付与 × ×
宣言時の型制限 あり ※2 なし なし

上記以外の違いとして「final」の機能はコメントでご指摘頂いている通り、再代入の禁止になります。
C#は基本的に宣言場所以外での代入をすることができないため、これに該当する機能はありません。
強いて言うのであれば、ローカル変数でも宣言できる「readonly」といったところでしょうか。
もちろんこんな機能はないため是非、C#にもほしい機能です。

※1 readonlyは宣言時以外、コンストラクタ内でのみ代入可能
※2 宣言できる型についてはこちらconst キーワード - C# リファレンス _ Microsoft Docs

参考にしたページ

Java(tm) Platform, Standard Edition 8 API仕様
Javaのfinalを大解剖 finalの全てがここにある!!

最後に

C#とJavaは言語のベースが同じだから似ていると聞いたことはありました。調べていく中で予約語に同じものが多かったりと確かに似ていると感じました。
また、実際にコードを書かないと気づけない違い(予約語が前後するなどの小さな違い)があったため、知識をため込むだけでなく、アウトプットのすることの大事さを改めて感じました。
ただ似た書き方をしていながら、異なる動きをする場合があるため、エラーが起きた場合に嵌らないようにしたいですね。

あと、何より情報のアップデート。。指摘して頂いた方々ありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自動実装プロパティのget/setの速さについて調べた話

はじめに

自動実装プロパティのget/setの速さってフィールドの読み書きと比べてどうなんだろう?」と思ったので簡単に調べました。

環境

Windows 8.1 (変なの使っててスミマセン)
Microsoft Visual Studio Community 2019 Version 16.4.3
.Net Framework 4.7.2
ILSpy version 5.0.2.5153
BenchmarkDotNet v0.12.0

プロパティの操作 ≒ メソッド呼び出し

 プロパティに対してはフィールドのようにアクセスできますが、実際にはメソッドの呼び出しが行われます。フィールドの場合はメソッドの呼び出しは行われません。(なので私はフィールドへの読み書きの方が自動実装プロパティのget/setより少し速いだろうと思っていました)

 プロパティに対する読み書きがメソッド呼び出しになっていることを確かめてみましょう。下のコードをビルドし、ILのコードを確認します。

using System;

namespace ConsoleApp11
{
    public class Person
    {
        /// <summary>
        /// プロパティ
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// フィールド (パブリックフィールドはホントは使っちゃダメだぞ)
        /// </summary>
        public int Age;
    }

    class Program
    {
        static void Main(string[] args)
        {
            var p = new Person() { Name = "taro", Age = 8};

            Console.WriteLine(p.Name);
            Console.WriteLine(p.Age);
        }
    }
}

上のコードをビルドして作ったexeファイルをILSpyで開くとILのコードを見ることができます。Nameプロパティへの読み書きはget_Nameとset_Nameになっていることが分かります。Ageフィールドへの読み書きはstfldldfldという命令になっています。
逆コンパイル1.PNG

インライン化

 プロパティへの読み書きはメソッド呼び出しとなり、フィールドへの読み書きはメモリの操作になることが分かりました。メソッド呼び出しを行う分、プロパティの方がフィールドよりも遅くなりそうな気がしますが、ILがネイティブコードに変換される過程がまだ残っています。

 プログラムを実行するとき、ILはランタイムによってネイティブコードに変換されます(JITコンパイル)。そしてこのときに、メソッド呼び出しのコードが呼び出し先のメソッド中にある処理に置き換えられることがあります(インライン化)。 

 インライン化が行われる条件について正確なことは分からないのですが、自動実装プロパティのget/setのように、処理内容が単純な場合はインライン化されるようです。

 コードを使って少し実験してみます。まずはインライン化を行わない場合です。Nameプロパティに[MethodImpl(MethodImplOptions.NoInlining)]属性を付けてインライン化を抑制します。

public class Person
{
    public string Name
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        get;
        [MethodImpl(MethodImplOptions.NoInlining)]
        set;
    }
    public int Age;
}

NameプロパティのsetのJITコンパイル後の内コードを見てみます。プロパティに値を代入する箇所にブレークポイントを置き、処理が止まったら逆アセンブリウィンドウを開きます。逆アセンブリウィンドウ上でもC#のコードのようにステップ実行できるのでブレークポイントの後ろにあるcall命令のところまで処理を進めます。

逆アセンブル.PNG

call命令の直前まで来たらステップインします。
逆アセンブル2.PNG

ステップインするとsetメソッドの中に入れますが、入った先にもう一つcall命令があります。2つ目のcall命令を実行するとプロパティの値が書き変わります。ただ、2つ目のcall命令のジャンプ先には逆アセンブリウィンドウからの操作では進めず、これ以上のことは分かりませんでした。(理由はよく分からない)

 次にインライン化を有効にした場合を見てみます。Nameプロパティに付ける属性の内容を変えます。

public class Person
{
    public string Name
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        get;
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        set;
    }
    public int Age;
}

先ほどと同じようにNameプロパティに値を設定するところで処理を止めて様子を見てみます。
逆アセンブル3.PNG

call 7467EC30は先ほどのインライン化させない場合のときの、2つ目のcall命令の内容と一致しています。また、1つ目のcall命令は見当たらなくなりました。恐らくですがインライン化により削除されたのではないかと思います。

ベンチマーク

インライン化の雰囲気を感じられたところで、プロパティとフィールドに対する読み書きの時間を比べてみようと思います。ベンチマークにはBenchmarkDotNet を使いました。以下のコードをリリースビルドし、実行します。

using System.Linq;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace ConsoleApp10
{
    public class PropertyVsField
    {
        public int AggressiveInlining
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get;

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            set;
        }

        public int NoInlining
        {
            [MethodImpl(MethodImplOptions.NoInlining)]
            get;

            [MethodImpl(MethodImplOptions.NoInlining)]
            set;
        }

        public int Default
        {
            get;
            set;
        }

        public int Field;
    }

    public class PropBenchmark
    {
        public PropertyVsField propVsField = new PropertyVsField()
        {
            AggressiveInlining = 42,
            NoInlining = 42,
            Default = 42,
            Field = 42
        };

        [Benchmark]
        public int AggressiveInlining()
        {
            int v = 0;
            foreach (var _ in Enumerable.Range(1, 10))
            {
                v += this.propVsField.AggressiveInlining;
            }

            return v;
        }

        [Benchmark]
        public int NoInlining()
        {
            int v = 0;
            foreach (var _ in Enumerable.Range(1, 10))
            {
                v += this.propVsField.NoInlining;
            }

            return v;
        }

        [Benchmark]
        public int Default()
        {
            int v = 0;
            foreach (var _ in Enumerable.Range(1, 10))
            {
                v += this.propVsField.AggressiveInlining;
            }

            return v;
        }

        [Benchmark]
        public int Field()
        {
            int v = 0;
            foreach (var _ in Enumerable.Range(1, 10))
            {
                v += this.propVsField.AggressiveInlining;
            }

            return v;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<PropBenchmark>();
        }
    }
}

※1回のgetの呼び出しだと処理時間が短すぎて差が確認できなかったのでforeachで回しています
※getとsetで結果に違いが出るとは考えにくかったのでgetだけ調べました。(値の設定はフィールドの方が速いけど、参照はプロパティの方が速いとかそんなことにはならないだろうと思った。setではなくgetにした理由は特にないです。)

実行結果は私の環境ではこんな感じになりました。
ベンチマーク.PNG
NoInlining以外は大体同じくらいの時間(Mean)になっているので、AggressiveInliningを指定しなくてもインライン化が行われてフィールドへの操作と同じくらいの効率でプロパティへのアクセスができていることが分かります。

参考

自動実装するプロパティ (C# プログラミング ガイド)
フィールド (C# プログラミング ガイド)
More Effective C# 6.0/7.0 項目1 アクセス可能なデータメンバーの代わりにプロパティを使おう
++C++; [フレームワーク / 実行環境] JITコンパイル
++C++; [構造化] [雑記] インライン化
Visual Studio デバッガーでの逆アセンブリ コードの表示
ILSpy
BenchmarkDotNet

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#]非同期メソッドにいちいちCancellationTokenを渡すのがめんどくさい

この記事は非同期メソッドにCancellationTokenをいちいち渡すことがめんどくさいという話とそれを楽にするための考察について書きます。
実用的かどうかは微妙なので注意してください。

はじめに

C#で非同期メソッドを使用するときキャンセルするためにはCancellationTokenを引数で渡す必要があります。
渡さなかった場合キャンセルできないため思いもよらぬバグに遭遇することがあります。
例えば以下のようなコードです。
(Unity用のコードですがだいたい察せると思います。)

// 1秒間隔で表示を更新する
class Hoge : MonoBehaviour
{
    [SerializeField]
    private TextMeshProUGUI text = default;

    void Start()
    {
        _ = HogeAsync();
    }

    async UniTask HogeAsync()
    {
        for (var i = 0; ; i++)
        {
            await UniTask.Delay(1000);
            text.text = $"count:{i}";
        }
    }
}

このコードはHogeAsyncを止める手段がありません。
これによってシーン遷移などでtextが破棄された後にtextにアクセスしてしまうということが起こります。

CancellationTokenを渡すように修正すると以下のようになります。

class Hoge : MonoBehaviour
{
    [SerializeField]
    private TextMeshProUGUI text = default;

    void Start()
    {
        // 破棄されるときにキャンセル状態になるCancellationToken
        // thisとtextの寿命が違う場合はこれではまずいがとりあえず一緒とする
        var cancellationToken = this.GetCancellationTokenOnDestroy();
        _ = HogeAsync(cancellationToken);
    }

    async UniTask HogeAsync(CancellationToken cancellationToken)
    {
        for (var i = 0; ; i++)
        {
            await UniTask.Delay(1000, cancellationToken: cancellationToken);
            text.text = $"count:{i}";
        }
    }
}

従来のコルーチンを使用した方法では自動的に寿命がゲームオブジェクトと結びついていたので非同期にすると少し面倒になっているように感じます。

処理が長くなり複数の非同期メソッドを使用する場合はCancellationTokenを渡し忘れないようにする必要があります。
そもそもCancellationTokenを引数に取るオーバーロードがない場合はCancellationToken.ThrowIfCancellationRequested()を使用してキャンセルされているかどうかチェックする必要があります。

考察

確実にキャンセルされない/キャンセルできない処理、カジュアルな用途の場合はCancellationTokenを渡さないという選択肢もありだと思います。
そうはいっても渡さなければいけないことも多いと思うので以下のような書き方を考えました。

static async YTask HogeAsync(CancellationToken token)
{
    // CancellationTokenを挿入する
    // この後の処理でawaitを使用するとawait抜ける際にキャンセル状態がチェックされるようになる
    await YTask.Inject(token);

    // 無限ループなのでキャンセルしないと終わらない
    for (var i = 0; ; i++)
    {
        Console.WriteLine(i);
        // CancellationTokenを渡してなくても勝手にキャンセルされる
        await Task.Delay(1000);
    }
}

実際に動かしてみると以下のような結果になります。

static async Task Main(string[] args)
{
    var cts = new CancellationTokenSource();
    // 5秒後にキャンセルする
    _ = Task.Run(async () =>
    {
        await Task.Delay(5000);
        cts.Cancel();
    });

    try
    {
        await HogeAsync(cts.Token);
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("catch OperationCanceledException");
    }
}

/*
0
1
2
3
4
catch OperationCanceledException
*/

動くコードはyaegaki/YTaskに置いています。

ポイントは戻り値のYTaskとYTask.Injectです。
これによって自動で後続のawaitの後にCancellationTokenの確認処理が差し込まれます。
最初にInjectしておけばawaitの度にいちいちCancellationTokenを渡さなくていいので多少楽になります。
しかし、この方法には以下のようなデメリットがあります。

  1. awaitした非同期メソッドをキャンセルできていない
  2. ネストした非同期メソッドに対応できない
  3. 同期的に完了したか最初から完了していたタスクをawaitした場合、キャンセルされない
  4. 結局最初にInjectを書かないといけなくて面倒

1について、この方法では必ずawaitの後でキャンセルの確認が行われるためawait対象の非同期メソッド自体はキャンセルされていません。
2について、Injectの引数としてCancellationTokenが必要なので結局CancellationTokenが必要になる点は変わっていません。
3について、実装上の制約です。(AsyncMethodBuilderのAwaitOnCompletedが呼ばれないため)
4について、これはそのままです。

Unityのようにシングルスレッドが前提でコルーチン的に使用する場合はstatic変数を使用すれば楽になりますがマルチスレッドになった瞬間崩壊します。

static async YTask FugaAsync(string name, CancellationToken token)
{
    // 最上位の非同期メソッドでstatic領域にInjectする
    await YTask.InjectToStatic(token);

    await Task.Delay(300);

    // await後にstatic領域のCancellationTokenがFugaAsyncの最初にInjectされたものに戻る
    // よって複数の非同期メソッドを別々のCancellationTokenで同時に動かしてもシングルスレッドの場合は正常に動作する

    await PiyoAsync(name);
}

static async YTask PiyoAsync(string name)
{
    // 下位の非同期メソッドではstatic領域から拾ってきてInjectする
    await YTask.InjectFromStatic();

    for (var i = 0; ; i++)
    {
        Debug.Log($"{name}:{i}");
        await Task.Delay(1000);
    }
}

まとめ

ちょっと思いついたので書いてみましたがよく考えると微妙でしたという感じです。
結局のところ毎回引数にCancellationTokenを渡すのはそういうものだと思って書くのが楽かもしれません。
Unityについてのみ考えるのなら非同期メソッドの先頭で常にGetCancellationTokenOnDestroyで取得したものをInjectするというのもありかもしれません。(うーん...:thinking:)

async YTask HogeAsync()
{
    await YTask.Inject(this.GetCancellationTokenOnDestroy());

    // FugaAsyncの中でも先頭でInjectしているはずなのでCancellationTokenを渡さない
    await FugaAsync();
}

async YTask FugaAsync()
{
    await YTask.Inject(this.GetCancellationTokenOnDestroy());

    // 適当な後続処理...
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ちょっと不思議なC#

struct A
{
    int sum;

    public int Sum(Action<int, int> log)
    {
        sum = 0;
        for (var i = 0; i < 10; i++)
        {
            sum += i;
            log(i, sum);
        }

        return sum;
    }
}

上記のクラスのSumメソッドは常に45を返してきそうな感じがしますね。
実はSumメソッドに渡す引数次第では他の値が返ってくる可能性があります。
どのような引数を与えれば他の値を返すことができるか少し考えてみてください。

答え(リフレクションあり)
var temp = new[]{(object)(new A())};
var sumMethod = typeof(A).GetMethod("Sum");
var sumField = typeof(A).GetField("sum", BindingFlags.NonPublic | BindingFlags.Instance);
var result = sumMethod.Invoke(temp[0], new object[]
{
    new Action<int, int>((i, sum) =>
    {
        sumField.SetValue(temp[0], 999);
        Console.WriteLine($"i:{i} sum:{sum}");
    }),
});
Console.WriteLine(result);
/*
i:0 sum:0
i:1 sum:1000
i:2 sum:1001
i:3 sum:1002
i:4 sum:1003
i:5 sum:1004
i:6 sum:1005
i:7 sum:1006
i:8 sum:1007
i:9 sum:1008
999
*/

答え(リフレクションなし)
var a = new A();
var result = a.Sum((i, sum) =>
{
    a = new A();
    Console.WriteLine($"i:{i} sum:{sum}");
});
Console.WriteLine(result);
/*
i:0 sum:0
i:1 sum:1
i:2 sum:2
i:3 sum:3
i:4 sum:4
i:5 sum:5
i:6 sum:6
i:7 sum:7
i:8 sum:8
i:9 sum:9
0
*/

どちらの解答も思いついた方は相当C#に詳しい方だと思います。
こんなこと知ってなくてもC#を書くことはできますがもしかしたら役に立つときが来るかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ちょっと不思議なC#クイズ

Q1

struct A
{
    int sum;

    public int Sum(Action<int, int> log)
    {
        sum = 0;
        for (var i = 0; i < 10; i++)
        {
            sum += i;
            log(i, sum);
        }

        return sum;
    }
}

上記のクラスのSumメソッドは常に45を返してきそうな感じがしますね。
実はSumメソッドに渡す引数次第では他の値が返ってくる可能性があります。
どのような引数を与えれば他の値を返すことができるでしょうか。

答え(リフレクションあり)
var temp = new[]{(object)(new A())};
var sumMethod = typeof(A).GetMethod("Sum");
var sumField = typeof(A).GetField("sum", BindingFlags.NonPublic | BindingFlags.Instance);
var result = sumMethod.Invoke(temp[0], new object[]
{
    new Action<int, int>((i, sum) =>
    {
        sumField.SetValue(temp[0], 999);
        Console.WriteLine($"i:{i} sum:{sum}");
    }),
});
Console.WriteLine(result);
/*
i:0 sum:0
i:1 sum:1000
i:2 sum:1001
i:3 sum:1002
i:4 sum:1003
i:5 sum:1004
i:6 sum:1005
i:7 sum:1006
i:8 sum:1007
i:9 sum:1008
999
*/

答え(リフレクションなし)
var a = new A();
var result = a.Sum((i, sum) =>
{
    a = new A();
    Console.WriteLine($"i:{i} sum:{sum}");
});
Console.WriteLine(result);
/*
i:0 sum:0
i:1 sum:1
i:2 sum:2
i:3 sum:3
i:4 sum:4
i:5 sum:5
i:6 sum:6
i:7 sum:7
i:8 sum:8
i:9 sum:9
0
*/

Q2

interface ITest
{
}

struct A
{
    int elemCount;

    public int Init<T>(Span<T> s)
        where T : ITest
    {
        elemCount = 0;
        foreach (ref var i in s)
        {
            elemCount++;
            i = default;
        }
        return elemCount;
    }
}

似たような問題です。
Initメソッドに要素数1以上のSpanを渡した場合、必ず1以上の値が返ってきそうな感じがします。
これも引数次第では1以上の値が返ってこない可能性があります。
どのような引数を与えればいいでしょうか。

答え
struct AA : ITest
{
    public A a;
}

static void Do()
{
    Span<AA> s = stackalloc AA[1];
    var result = s[0].a.Init(s);
    Console.WriteLine(result);
}
/*
0
*/

最後に

どちらの解答も思いついた方は相当C#に詳しい方だと思います。
こんなこと知ってなくてもC#を書くことはできますがもしかしたら役に立つときが来るかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数のコンソールアプリケーションを呼び出す簡易ワークフローを作ってみる

このドキュメントの内容

業務システムでは他システム連携や実績データ集計などを夜間バッチで運用することが比較的多いです。システムの規模に比例して夜間バッチアプリケーションの数も多くなり、ジョブ管理ソフトを導入したりするわけですが、決して安いものではありません。また、ソフトがサポートしている全ての機能が必要かというとそうでもないケースが少なくありません。
そこで、シンプルなワークフローの仕組みを作ってみます。

なお、プロセス呼び出しには .NET の Process クラスではなく、先日公開されたOSSの ProcessX を使ってみます。C#8.0 の非同期ストリームを用いて標準出力を受け取ることができたり、async-await なプロセス呼び出しが可能になります。
ProcessX - C#でProcessを C# 8.0非同期ストリームで簡単に扱うライブラリ

実装予定の機能

終了コードによる分岐

コンソールアプリケーションの終了コードによる分岐を実現します。終了コードが 0 のときは成功、0 でないときは失敗とみなします。

ワークフロー上の処理単位を「ワークフローアイテム」と考え、次のメンバを定義します。

  • 処理を実行するメソッド。戻り値で終了コードを返します。
  • 処理が成功したときの次処理を表すワークフローアイテムと、処理が失敗したときの次処理を表すワークフローアイテムを表すプロパティ。単方向のリンクリスト構造とします。
  • 終了コードごとの次処理を表すワークフローアイテムを持たせるかどうかも検討しています。
ワークフローアイテムを表すインターフェース
/// <summary>
/// ワークフローアイテムに必要な機能を提供します。
/// </summary>
public interface IWorkflowItem
{
    /// <summary>
    /// 処理を実行します。
    /// </summary>
    /// <param name="prevExitCode">前処理の終了コード</param>
    /// <returns>終了コード</returns>
    Task<int> ExecuteAsync(int prevExitCode);

    /// <summary>
    /// 処理が成功したときの次処理を取得または設定します。
    /// </summary>
    IWorkflowItem NextOnSucceed { get; set; }

    /// <summary>
    /// 処理が失敗したときの次処理を取得または設定します。
    /// </summary>
    IWorkflowItem NextOnFailed { get; set; }
}

直列実行

一つのコンソールアプリケーションを実行する処理を「コマンド」と考え、次のメンバを定義します。

  • 処理を実行するメソッド。戻り値で ProcessX で提供されている非同期ストリームオブジェクトを返します。このメソッドの中でプロセス呼び出しを行うように実装します。

ワークフローアイテムの中にコマンドを複数格納し、直列実行します。

コマンドを表すインターフェース
/// <summary>
/// コマンドに必要な機能を提供します。
/// </summary>
public interface IProcessCommand
{
    /// <summary>
    /// 処理を開始します。
    /// </summary>
    /// <param name="prevExitCode">前処理の終了コード</param>
    /// <returns>標準出力を返す非同期シーケンス</returns>
    ProcessAsyncEnumerable StartAsync(int prevExitCode);
}

並列実行

ワークフローアイテムの中にコマンドを複数格納するのは直列実行と同じです。
Task.WhenAll メソッドを用いて並列実行します。

直列/並列実行の終了コード判定

ワークフローアイテム内で複数のコマンド(=プロセス呼び出し)を実行する場合、それぞれのコマンドの終了コードからワークフローアイテム単位の終了コードを決定できる仕組みを実装します。

終了コード決定処理を表すインターフェース
/// <summary>
/// 終了コードの制御に必要な機能を提供します。
/// </summary>
public interface IExitCodeHandler
{
    /// <summary>
    /// 指定されたコマンドの実行結果から終了コードを決定します。
    /// </summary>
    /// <param name="results">コマンドの実行結果</param>
    /// <returns>終了コード</returns>
    int HandleExitCode(ProcessCommandResult[] results);
}

/// <summary>
/// コマンドの実行結果。
/// </summary>
public readonly struct ProcessCommandResult
{
    public ProcessCommandResult(IProcessCommand command, int exitCode)
    {
        Command = command;
        ExitCode = exitCode;
    }

    /// <summary>
    /// コマンドを取得します。
    /// </summary>
    public IProcessCommand Command { get; }

    /// <summary>
    /// 終了コードを取得します。
    /// </summary>
    public int ExitCode { get; }
}

設定ファイルによるワークフローの組み立て

前述のワークフローアイテム、コマンド、終了コード制御の設定値を設定ファイルから読み込み、ワークフローを組み立てられるようにします。

  • ワークフローアイテムのリンクリスト構造
  • 実行するコマンドライン文字列
  • 環境変数として渡すキーと値の組み合わせ
  • 終了コードの制御

サンプルコード

現時点のソースコードは GitHub で見ることができます。但し、検討初期段階のため、破壊的変更を含む大幅な変更を行う可能性が非常に高いです。

ワークフロー実行アプリケーション

ワークフローアイテムのリンクリスト構造を組み立て、起点となるワークフローアイテムを実行します。
次のコードではコード上でワークフローアイテムを組み立てていますが、設定ファイル読み込みによって同等の内容を実現できるように検討しています。

class Program
{
    static async Task Main(string[] args)
    {

        string filePath = @"d:\SampleApp1.exe";

        var item = new ParallelWorkflowItem("root", "最初の処理")
        {
            // 実行するコマンド(並列実行)
            Commands = new[]{
                ProcessCommandFactory.FromCommandLine("command1", "コマンド1", filePath + " 5")
                , ProcessCommandFactory.FromCommandLine("command2", "コマンド2", filePath + " 4")
            }
            ,
            // 終了コードの制御は既定
            // 何れかのコマンドの終了コードが 0 でない場合、最初に見つかった終了コードを返す
            ExitCodeHandler = null
            ,
            // 成功時の次処理
            NextOnSucceed = new SequencialWorkflowItem("root-succeed", "成功時の後処理")
            {
                // 実行するコマンド(直列実行)
                Commands = new[]
                {
                    ProcessCommandFactory.FromFile("command3", "コマンド3", filePath, "3")
                    , ProcessCommandFactory.FromFile("command4", "コマンド4", filePath, "-2")
                }
                ,
                // 終了コードの制御
                ExitCodeHandler = ExitCodeHandlerFactory.Create(
                    // 既定の終了コード
                    -1
                    , new[]
                    {
                        // command3 の終了コード = 0 && command4 の終了コード = 0 => 0
                        (new[] { ("command3", 0), ("command4", 0) }, 0)
                        // command3 の終了コード = 1 && command4 の終了コード = 1 => 11
                        , (new[] { ("command3", 1), ("command4", 1) }, 11)
                        // command4 の終了コード = 1 => 10
                        , (new[] { ("command4", 1) }, 10)
                    }
                )
                ,
                // 成功時の次処理
                NextOnSucceed = new WorkflowItem("root-succeed-succeed", "成功時の後処理")
                {
                    Command = ProcessCommandFactory.FromFile("command5", "コマンド5", filePath, "1")
                }
                ,
                // 失敗時の次処理
                NextOnFailed = new WorkflowItem("root-succeed-failed", "失敗時の後処理")
                {
                    // 実行するコマンド
                    Command = ProcessCommandFactory.FromFile("command6", "コマンド6", filePath, "2")
                }
            }
            ,
            // 失敗時の次処理
            NextOnFailed = new WorkflowItem("root-failed", "失敗時の後処理")
            {
                // 実行するコマンド
                Command = ProcessCommandFactory.FromFile("command7", "コマンド7", filePath, "1")
            }
        };


        // ワークフローを実行する
        IWorkflowItem current = item;
        int exitCode = 0;

        try
        {
            while (current != null)
            {
                Console.WriteLine($"----- {current.ID}:{current.Name} -----");
                exitCode = await current.ExecuteAsync(exitCode);
                Console.WriteLine($"{current.ID}.ExitCode = {exitCode}");

                if (exitCode == 0)
                {
                    current = current.NextOnSucceed;
                }
                else
                {
                    current = current.NextOnFailed;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

呼び出すコンソールアプリケーション

SampleApp1
class Program
{
    static async Task<int> Main(string[] args)
    {
        int exitCode = await MainAsync(args);
        Console.WriteLine($"ExitCode = {exitCode}");
        return exitCode;
    }

    private static async Task<int> MainAsync(string[] args)
    {

        // 環境変数から前処理の終了コードを取得して出力
        var variables = System.Environment.GetEnvironmentVariables();
        var key = ProcessCommandVariables.PrevExitCode;
        Console.WriteLine($"EnvironmentVariables[{key}] = {variables[key]}");

        // 引数を出力
        for (int i = 0; i < args.Length; ++i)
        {
            Console.WriteLine($"args[{i}] = {args[i]}");
        }

        // 先頭の引数は繰り返し回数を表すものとする
        int repeatCount = Convert.ToInt32(args[0]);

        if (repeatCount < 0) { return 1; }

        for (int i = 0; i < repeatCount; ++i)
        {
            await Task.Delay(1000);
            Console.WriteLine($"{i + 1}/{repeatCount}");
        }

        return 0;

    }

}

出力結果

----- root:最初の処理 -----
[コマンド1] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 0
[コマンド1] args[0] = 5
[コマンド2] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 0
[コマンド2] args[0] = 4
[コマンド1] 1/5
[コマンド2] 1/4
[コマンド1] 2/5
[コマンド2] 2/4
[コマンド1] 3/5
[コマンド2] 3/4
[コマンド1] 4/5
[コマンド2] 4/4
[コマンド2] ExitCode = 0
[コマンド1] 5/5
[コマンド1] ExitCode = 0
root.ExitCode = 0
----- root-succeed:成功時の後処理 -----
[コマンド3] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 0
[コマンド3] args[0] = 3
[コマンド3] 1/3
[コマンド3] 2/3
[コマンド3] 3/3
[コマンド3] ExitCode = 0
[コマンド4] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 0
[コマンド4] args[0] = -2
[コマンド4] ExitCode = 1
Process returns error, ExitCode:1

root-succeed.ExitCode = 10
----- root-succeed-failed:失敗時の後処理 -----
[コマンド6] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 10
[コマンド6] args[0] = 2
[コマンド6] 1/2
[コマンド6] 2/2
[コマンド6] ExitCode = 0
root-succeed-failed.ExitCode = 0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む