yotiky Tech Blog

とあるエンジニアの備忘録

C# 9.0 の主な新機能

目次

リリース時期

  • .NET 5

init 専用セッター

public record WeatherObservation
{
    // set の代わりに初期化 init が使える
    public DateTime RecordedAt { get; init; }
    
    public decimal TemperatureInCelsius { get; init; }

    public readonly decimal PressureInMillibars = 1000.0m;
    
    public WeatherObservation()
    {
        // init アクセサーのプロパティはコンストラクタかオブジェクト初期化子でのみ値を設定できる
        TemperatureInCelsius = 10;
        // readonly なフィールドはフィールド初期化子かコンストラクタで初期化する
        PressureInMillibars = 1000.0m;
    }
}
public class Features
{
    public static void Init専用セッター()
    {
        var now = new WeatherObservation
        {
            RecordedAt = DateTime.Now,
            TemperatureInCelsius = 20,
            // readonly なフィールドにはオブジェクト初期化子では書き込みできない
            //PressureInMillibars = 998.0m,
        };

        // 初期化後は値を変更できない
        //now.RecordedAt = DateTime.MaxValue;
    }
}

Records

// レコードはデフォルトでは immutable なクラスに展開される
public record Person
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) 
        => (FirstName, LastName) = (first, last);
}

// レコードは継承することも、sealed にすることも可能
public record Teacher : Person
{
    public string Subject { get; }

    public Teacher(string first, string last, string sub) : base(first, last)
        => Subject = sub;
}

// Positional records / 位置指定の初期化子 と呼ばれる完結な形式で定義できる
public record Pet(string Name, string Type);
public static void Records()
{
    // レコードはデフォルトでは immutable
    // セッターがないので値を設定できない
    var p1 = new Person("Taro", "Saito");
    //p1.FirstName = "Ichiro";

    // 比較は同じインスタンスかではなく、プロパティの値が一致するか
    var p2 = new Person("Taro", "Saito");
    Debug.WriteLine($"p1==p2 : {p1 == p2}");
    //【出力】p1 == p2 : True

    // ToString() は独自に加工されている
    Debug.WriteLine(p1.ToString());
    //【出力】Person { LastName = Saito, FirstName = Taro }

    // Positional records で定義すると init 専用プロパティとなるので値を設定できない
    var cat = new Pet("Pochi", "Cat");
    // CS8852 init 専用プロパティまたはインデクサー 'Features.Pet.Name' を割り当てることができるのは、オブジェクト初期化子の中か、インスタンス コンストラクターまたは 'init' アクセサーの 'this' か 'base' 上のみです。
    //cat.Name = "Tama";

    // with を使うと値を上書きしたコピーを作成できる
    var cat2 = cat with { Name = "Tama" };
    Debug.WriteLine(cat2.ToString());
    //【出力】Pet { Name = Tama, Type = Cat }

    // プロパティの分解もできる
    var (name, type) = cat2;
    Debug.WriteLine($"{type}の{name}。");
    //【出力】CatのTama。
}

トップ レベル ステートメント

using System;
using System.Threading.Tasks;

// エントリーポイントとして展開される、だからプロジェクトで1ファイルのみ
// 名前空間やクラスよりも上に書く必要がある
Console.WriteLine("Hello world!");

// 暗黙的に引数 args を使える
Console.WriteLine(args.Length);

// メソッドも定義できる
// ローカル関数に展開される、他からアクセスできない
void m1(string s)
{
    Console.WriteLine(s);
}
void m2(string s) => Console.WriteLine(s);
m1("Hoge");
m2("Fuga");

// async/await も使える
await Task.Delay(1000);

// 暗黙的に戻り値 int を使える
return 0; 

public class TopLevelStatements
{
    public static void Features()
    {
        // このコンテキストでは、トップレベルのステートメントで宣言されたローカル変数またはローカル関数 'm1' を使用することはできません。
        //m1("hogehoge");
    }
}

ターゲットからの new 型推論

// `new 型名()` の型名を省略できる
// フィールドやメソッドの引数などで使えるが、var は推論できないので使えない
Dictionary<string, string> _dic = new();

Dictionary<string, List<(int x, int y)>> _cache = new()
{
    { "A", new() { (1, 1), (2, 2) } },
    { "B", new() { (1, 1), (2, 2) } },
};

public static void ターゲットからのnew型推論()
{
    // メソッドの引数の型がわかってるので使える
    ReturnNew(new());

    // これも型がわかるので使える
    (int X, int Y) p = new(1, 2);

    // これは Exception が飛ぶ
    throw new("This is Exception.");
}
private static List<int> ReturnNew(List<int> list)
{
    // 戻り値の型がわかってるので使える
    return new();
}

ラムダ式の引数を破棄

public static void ラムダ式の引数を破棄()
{
    Action<int, int> action = (_, _) =>
    {
        // CS0103 '_' は、現在のコンテキストに存在しません。
        //Console.WriteLine(_);
    };

    Action<int, int> action2 = (_, _1) =>
    {
        // 引数 _ が1個だけの場合は破棄されない
        Console.WriteLine(_);
    };
}

ローカル関数への属性適用

public static void ローカル関数への属性適用()
{
    // ローカル関数に属性を付けれる
    [return: NotNullIfNotNull("s")]
    string? toLower(string? s) => s?.ToLower();

}

パターンマッチングの拡張機能

public static void パターンマッチングの拡張機能()
{
    var x = 80;

    // 先頭の変数に対して条件だけを書ける
    // 変数が長かったりプロパティの深い入れ子の場合に有用そう
    if (x is (>= 0 and < 10) or (> 80 and <= 100)) { }

    // == ではなく直接値を指定する
    if (x is 80 or > 80) { }

    // 関数は1回の実行だけで済む
    Func<int> func = () => 5;
    if (func() is > 0 and < 10) { }

    string s = null;

    // s != null と同じ
    if (s is not null) { }
}

コード置き場

github.com

Unity - スライダーでズームイン/ズームアウトする

対象のオブジェクトを中心に、スライダーの値でズームインしたりズームアウトするサンプルスクリプト。 スライダーの値に応じてカメラが移動する。

f:id:yotiky:20201121020630g:plain

public class Zoomer: MonoBehaviour
{
    public GameObject playerObject;
    public Slider zoomSlider;
    public float minDistance = 1;
    public float maxDistance = 5;

    private Camera mainCamera;

    void Start()
    {
        mainCamera = Camera.main;
        // カメラの位置を可動域に収めるため1度呼び出す
        Zoom();
    }

    public void Zoom()
    {
        // 最大距離から見ての目標距離
        var targetDistance = maxDistance - (maxDistance - minDistance) * zoomSlider.value;
        // playerObject からカメラへの向きと距離
        var headingPtoC = mainCamera.transform.position - playerObject.transform.position;
        // 方向に距離を掛けて目的の位置を計算
        mainCamera.transform.position = headingPtoC.normalized * targetDistance;
    }
}

画面の方は、uGUI で Screen Space - Overlay で右上からスライダーを追加してる。

f:id:yotiky:20201121012005p:plain

スライダーは、0~1 で定義し、可動域の中でどれだけの割合かを示すようにしている。

f:id:yotiky:20201121021249p:plain

Unity - スライダーで対象を中心にカメラを回転させる(三人称視点)

対象のGameObjectを中心にカメラをスライダーの値で回転(三人称視点)させるサンプルスクリプト。 回転可能な角度を設定して正面からの回転角度に制限を設けている。

f:id:yotiky:20201121011440g:plain

    public GameObject playerObject;
    public Slider rotationSlider;
    public float maxAngle = 90;

    private Camera mainCamera;
    private float totalAngleX;

    void Start()
    {
        mainCamera = Camera.main;
    }

    public void Rotation()
    {
        var newAnglex = maxAngle * rotationSlider.value - totalAngleX;

        totalAngleX += newAnglex;

        mainCamera.transform.RotateAround(playerObject.transform.position, Vector3.up, newAnglex);
    }

画面の方は、uGUI で Screen Space - Overlay で右上からスライダーを追加してる。

f:id:yotiky:20201121012005p:plain

スライダーは、-1~1 で定義し、回転可能角度を掛けることで回転する量を計算している。

f:id:yotiky:20201121012116p:plain

WPF から Application Insights に Telemetry を送信する

目次

Application Insights

Application Insights は Azure Monitor の機能であり、開発者や DevOps プロフェッショナル向けの拡張可能なアプリケーション パフォーマンス管理 (APM) サービスです。 このサービスを使用して、実行中のアプリケーションを監視することができます。 パフォーマンスの異常を自動的に検出し、組み込まれている強力な分析ツールを使用して、問題を診断し、ユーザーがアプリを使用して実行している操作を把握できます。 Application Insights は、パフォーマンスやユーザビリティを継続的に向上させるうえで役立つように設計されています。 オンプレミス、ハイブリッド、または任意のパブリック クラウドでホストされている .NET、Node.js、JavaPython などのさまざまなプラットフォーム上のアプリで機能します。 DevOps プロセスと統合され、さまざまなツールへの接続ポイントを備えています。 Visual Studio App Center と統合することで、モバイル アプリからテレメトリを監視および分析できます。

docs.microsoft.com

Application Insights は、Azure プラットフォーム上に限った話ではなく、ASP.NET/ASP.NET Core Web アプリケーション以外に JavaJavaScript、Node.JS、Pyhon などの言語や、AndroidiOS向けにもSDKが提供されています。

今回は、WPF から Telemetry を送信する方法を紹介します。

非HTTPアプリケーション向け

docs.microsoft.com

上記サイトでは、Webアプリケーション以外の3種類のアプリケーションについて説明しています。

  1. .NET Core 3.0 ワーカーサービス
    .NET Core 3.0 でマイクロサービス関連のバックグラウンド処理を目的とした Worker Service プロジェクトが追加されました。実行時間が長いサービスを、Windows サービスとして実行します。

  2. IHostedService を使用した ASP.NET Core のバックグラウンドタスク .NET Core 2.0 以降 IHostedService が提供され、バックグラウンドプロセスで処理するタスクを簡単に実装できるようになっています。

  3. .NET Core / .NET Framework コンソール アプリケーション .NET Core 2.0 以上、もしくは .NET Framework 4.7.2 以上で利用できます。コンソールアプリケーションとなっていますが、WPFでも使用可能です。

検証環境

手順

パッケージのインストール

NuGet で Microsoft.ApplicationInsights.WorkerService をインストールします。 間違えずに WorkerService が付いているものを選びます。

実装

まずは全文です。

IServiceCollection services = new ServiceCollection();
services.AddLogging(builder => builder.AddFilter<ApplicationInsightsLoggerProvider>("WpfApp1", LogLevel.Information));
services.AddApplicationInsightsTelemetryWorkerService("instrumentationkeyhere");

IServiceProvider serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<SampleClass>>();
var telemetryClient = serviceProvider.GetRequiredService<TelemetryClient>();

var httpClient = new HttpClient();

// 実際はTelemetryを送信する期間≒アプリケーションのライフサイクルに合ったロジックが必要
//while (true) 
{
    logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);

    using (telemetryClient.StartOperation<RequestTelemetry>("operation"))
    {
        logger.LogWarning("A sample warning message. By default, logs with severity Warning or higher is captured by Application Insights");
        logger.LogInformation("Calling bing.com");
        var res = await httpClient.GetAsync("https://bing.com");
        logger.LogInformation("Calling bing completed with status:" + res.StatusCode);
        telemetryClient.TrackEvent("Bing call event completed");
    }

    await Task.Delay(1000);
}

telemetryClient.Flush();
Task.Delay(5000).Wait();


続けて細部を見ていきます。

IServiceCollection services = new ServiceCollection();
services.AddLogging(builder => builder.AddFilter<ApplicationInsightsLoggerProvider>("WpfApp1", LogLevel.Information));
services.AddApplicationInsightsTelemetryWorkerService("instrumentationkeyhere");

ServiceCollection は、Microsoft.Extensions.DependencyInjection 名前空間にあるDIコンテナです。 そこに ApplicationInsightsLoggerProvider を使用して、ILogger のログをキャプチャするように設定します。

ILoggingBuilder でフィルターを追加しています。後で Logger を取得する時に Type をカテゴリ名として指定しますが、その Type の FullName のプレフィックスと一致させることで、出力するログレベルを制御することができます。

今回はアプリケーションの名前空間Wpf1App であり SampleClass もその配下に定義されています。また、LogLevelInformation なので、Information 以上の Level のログが出力されることになります。 一致するフィルターがない Logger の場合は、既定値の Warning 以上のログが出力されるようになります。

"instrumentationkeyhere" はよしなに取得、設定するようにして下さい。


ILogger のログをキャプチャする必要がなく、 TelemetryClient だけを使用する場合はよりシンプルになります。必要なのは次のコードの部分だけです。

IServiceCollection services = new ServiceCollection();
services.AddApplicationInsightsTelemetryWorkerService(key);

IServiceProvider serviceProvider = services.BuildServiceProvider();
var telemetryClient = serviceProvider.GetRequiredService<TelemetryClient>();


次に、ServiceProvider をビルドして、Loggerインスタンスを取得します。

IServiceProvider serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<SampleClass>>();
var telemetryClient = serviceProvider.GetRequiredService<TelemetryClient>();

カテゴリ名として指定する Type は、Application Insights にもカテゴリとして出力されます。 ILogger に定義されている汎用的なログ出力以外に、Application Insights で使われる詳細なログを出力するために、TelemetryClient を使います。


残りの後半部分についてです。

RequestTelemetryusing で囲ったログと依存関係が収集され親子関係を持つログとして関連付けられます。依存関係はログだけでなく、外部コンポーネント、つまりHTTP を使用して呼び出されるサービス、またはデータベース、あるいはファイル システムも追跡されます。追跡される依存関係をいかに示します。 また、StartOperation() を使う方法は、依存関係を手動で追跡する方法に該当します。

依存関係 詳細
Http/Https ローカルまたはリモートの http/https 呼び出し
WCF 呼び出し Http ベースのバインディングを使用する場合にのみ自動追跡されます。
SQL SqlClient で行われる呼び出し。 SQL クエリのキャプチャについてはこちらを参照してください。
Azure Storage (BLOB、テーブル、キュー) Azure Storage Client で行われる呼び出し。
EventHub Client SDK バージョン 1.1.0 以上。
ServiceBus Client SDK バージョン 3.0.0 以上。
Azure Cosmos DB HTTP/HTTPS が使用されている場合にのみ、自動的に追跡されます。 TCP モードは、Application Insights ではキャプチャされません。

Application Insights での見え方は、後段の実行結果を確認してみてください。 「DEPENDENCY」として登録されているデータが追跡結果になります。

docs.microsoft.com

アプリケーションを終了する場合は、Telemetry が送信完了するために Flush() を呼び出す必要があります。通常は、30秒毎、または500アイテムが溜まってからデータを送信するようです。

実行結果

サンプルコードの出力結果です。

f:id:yotiky:20201118012613p:plain

以下は、RequestTelemetry の詳細です。 タイムラインで処理時間を見ることができます。また、RequestTelemetry で囲った内容だけが集約されているのが分かります。

f:id:yotiky:20201118014009p:plain f:id:yotiky:20201118013915p:plain

一致するフィルタがない場合は、 Warning 以上が出力されるようになります。

f:id:yotiky:20201118000223p:plain

WPF - DesignerProperties

DesignerProperties はデザインモードか判定に使われるクラスですが、手元の環境で上手く機能しなくなっていたので簡単に調査した結果です。

目次

検証環境

内容

使い方

XAML 上で直接デザイン時のみスタイルを変更する場合の例。

<d:DesignerProperties.DesignStyle>
    <Style TargetType="Window">
        <Setter Property="Background" Value="Black"/>
    </Style>
</d:DesignerProperties.DesignStyle>

コードビハインドを使う場合の例。

if (DesignerProperties.GetIsInDesignMode(this))
{
    this.Background = Brushes.Blue;
}

UIエレメントが取得できない場所(ViewModelなど)で使う場合の例。

if ((bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
{
    this.Background = Brushes.Red;
}

動作確認

WPF (.NET Framework)

.NET FrameworkWPF アプリですが、XAMLでの設定は使えますが、コード側からの2パターンは機能しません。判定は有効なようですが、コントロールへの変更が動いてないように見えます。

引き続きコード側で、判定した上で処理する内容を切り替えるといった用途には使えます。

WPF (.NET Core)

.NET Core の WPF アプリですが、XAMLも含めすべてがデザイナには機能しません。 そもそもXAMLで警告が出てしまいます。

f:id:yotiky:20201111031348p:plain

.NET Framework 同様コード側での判定処理は使えます。

WPF - CommandLocker

Button にバインディングしたコマンドを共通してロックする機構 CommandLocker。

github.com

ロックの種類は、Guidをキーとしたシンプルなロックと、ロックする時に渡した値をアンロックする際にも渡す必要があるトリガーロックの2種類です。

トリガーロックは、コマンド実行後に「ステータスの変更を監視してロックを解除する」非同期なロックを想定しています。

トリガーロックよりシンプルなロックの方が強くなっています。 トリガーロックで非同期待ちしている状態でも、シンプルなロックを共有するコマンドは実行可能となります。逆に、シンプルなロック中はトリガーロックを共有するコマンドは実行できません。トリガーロックを待つ必要があるコマンドは、トリガーロックを共有してください。

使い方

CommandLocker を共通でロックしたい Command に共有します。

var locker = new CommandLocker<int>();

DefaultLock1Command = new ReactiveCommand(locker.CanExecute);
DefaultLock2Command = new ReactiveCommand(locker.CanExecute);
TriggerLock1Command = new ReactiveCommand(locker.CanExecuteWithTrigger);
TriggerLock2Command = new ReactiveCommand(locker.CanExecuteWithTrigger);

シンプルなロックは、 Lock() に成功したら実処理を実行し、Release() 時にキーを渡します。

var locked = locker.Lock();
if (!locked.result) { return; }

// なにかの処理

locker.Release(locked.key);

トリガーロックは、Lock() する時にトリガーとなる値を渡します。

var locked = locker.Lock(3);
if (!locked.result) { return; }

// なにかの処理

何かしらのステータスを監視し値が変更されたら、TryRelease()`でアンロックを試みます。

var result = locker.TryRelease(clickedCount);

トリガーロックは、ロックする時にアンロックを待つメソッドも用意しています。

var locked = locker.Lock(3);

// なにかの処理

await locker.WaitReleaseAsync();

// 後続処理

ロックが解除できなくなった場合のために、ReleaseForce() で強制アンロックができます。

locker.ReleaseForce();

C# - 固定サイズのキュー

ラッパークラス

最小限の実装。Queue がラップされるので Queue で必要な機能は全部中継する必要がある。

public class FixedSizeQueue<T>
{
    private ConcurrentQueue<T> queue = new ConcurrentQueue<T>();
    private object lockObject = new Object();
    public int Size { get; }

    public FixedSizeQueue(int size)
    {
        Size = size;
    }
    public void Enqueue(T item)
    {
        lock (lockObject)
        {
            queue.Enqueue(item);
            while (Size < queue.Count)
            {
                queue.TryDequeue(out var _);
            }
        }
    }
    public bool TryDequeue(out T result)
        => queue.TryDequeue(out result);
    public int Count()
        => queue.Count;
}

拡張メソッド

必要な機能中継するのが面倒な時に、Enueue する時に長さを指定する簡易機能。

public static class QueueExtensions
{
    public static void EnqueueFixedSize<T>(this Queue<T> queue, T item, int size)
    {
        queue.Enqueue(item);
        while (size < queue.Count)
        {
            queue.TryDequeue(out var _);
        }
    }
    public static void EnqueueFixedSize<T>(this ConcurrentQueue<T> queue, T item, int size, object lockObject)
    {
        lock (lockObject)
        {
            queue.Enqueue(item);
            while (size < queue.Count)
            {
                queue.TryDequeue(out var _);
            }
        }
    }
    public static void EnqueueFixedSize<T>(this ConcurrentQueue<T> queue, IEnumerable<T> items, int size, object lockObject)
    {
        lock (lockObject)
        {
            foreach (var item in items)
            {
                queue.Enqueue(item);
            }
            while (size < queue.Count)
            {
                queue.TryDequeue(out var _);
            }
        }
    }
}