rksoftware

Visual Studio とか C# とかが好きです

Surface Go 3 で スト5

ある時突然ストリートファイター5がやりたくなることが誰でもあると思います。
そして、手元には Surface Go 3(※) だけしかない。そんな日もたまにはあります。
※真ん中のスペックのモデル(最初に発売された2モデルの上位モデルの方)

■ ベンチマーク結果

ベンチマークは完走しました!
お、行けるか!? と思ったら結果は

STREET FIGHTER Ⅴ をプレイするには PC のスペックが不足しています。

画面品質を低にしてもでした。
無念。

The following component(s) are required to run this program: DirectX Runtime

ちょっとゲームのベンチマークを動かそうと思ったときに、動かなかったのでメモ。
スペック的に DirectX のバージョンは満たしている...と思っていたのですが次のエラーが。

The following component(s) are required to run this program:
DirectX Runtime 

f:id:rksoftware:20220108230507p:plain

■ 解決

次のサイトからランタイムをインストール f:id:rksoftware:20220108230922p:plain

これで動くようになりました。

構造体のプロパティに値をセットする

リフレクションを使って構造体のプロパティに値をセットする方法です。
クラスと同じように書いてはうまくいかなかったのでメモ。

■ 参照情報(答え)

ここを見ればすべてが解決します。

■ リンクをクリックするのが面倒な方へ

クラスと同じように書くと期待通りに行かない

Sample s = new Sample();
var type = typeof(Sample);
var p = type.GetProperty("PropA");
p.SetValue(s, "value");
Console.WriteLine(s.PropA); // 結果は null 期待通りにならない 

struct Sample
{
    public string PropA { get; set; }
}

期待通りの結果にする方法

構造体を Box 化します

Sample s = new Sample();
var type = typeof(Sample);
var p = type.GetProperty("PropA");
object o = (object)s;
p.SetValue(o, "value");
var s2 = (Sample)o;
Console.WriteLine(s.PropA); // 結果は null
Console.WriteLine(s2.PropA); // 結果は value 期待通り 

struct Sample
{
    public string PropA { get; set; }
}

ちょっとつらいですね。

コマンドライン引数を雑に扱う (4)

以前にコマンドライン引数を雑に扱う記事を書きました。

コンソールアプリ、ちょっとした処理を行う際に雑に作ってしまうのが結構いいのですが、何気にコマンドライン引数の解釈コードが面倒です。 それはもう、あらゆる要素をソースコードに書き込んで毎回ソースコード上の値を書き換えてビルドして使うほどに。

というわけで、引数を扱うコードを雑に書いてきたのですが、いつまでもいろいろ不完全だったりコードも雑すぎたので今後いじっていきやすいように NuGet パッケージ化してしまいました。

■ NuGet パッケージ

dotnet add package Rksoftware.ArgumentsBuilder

■ 引数レコード

解釈した引数の値を格納する record を用意します。

using Rksoftware.ArgumentsBuilder.Attributes;

record struct Arguments([Parameter] string? Aparam, [Parameter] string? Bparam, string? A, string? B, string? C, bool D, bool E) { }

Parameter という属性が付いている引数に、コマンドライン引数の何もない普通の引数が入ってきます。たとえば

<command> 1 2

として実行すると、引数 Aparam の値が 1 に、Bparam の値が 2 になります。

Parameter という属性が付いていない string? 型の引数は - や / で始まるオプション指定の値が入ってきます。たとえば

<command> -a 1  /b 2

として実行すると、引数 A の値が 1 に、B の値が 2 になります。

Parameter という属性が付いていない string? 型の引数は - や / で始まるオプション指定があった場合、true が入ってきます。たとえば

<command> -d /e

として実行すると、引数 D の値が true に、E の値が true になります。

■ 使用コード

引数レコード

using Rksoftware.ArgumentsBuilder.Attributes;

record struct Arguments([Parameter] string? Aparam, [Parameter] string? Bparam, string? A, string? B, string? C, bool D, bool E) { }

実行する処理を書くクラス

interface IService { }

class Service : IService
{
    public string Run(Arguments arguments) => arguments.ToString();
}

メインのプログラムクラス

ApplicationRun という拡張メソッドで、DI でとってくるインタフェースと引数レコードの型および引数を指定するだけで OK。
この例では ApplicationRun の中で IService インタフェースの実行クラスの中から Arguments 型の引数をもつメソッドを探して実行してくれます。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Rksoftware.ArgumentsBuilder.Hosts;

using IHost host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => services.AddSingleton<IService, Service>()).Build();
var result = host.ApplicationRun<IService, Arguments>(args);

Console.WriteLine((string?)result);

簡単ですね。

コマンドライン引数を雑に扱う (3)

以前にコマンドライン引数を雑に扱う記事を書きました。

コンソールアプリ、ちょっとした処理を行う際に雑に作ってしまうのが結構いいのですが、何気にコマンドライン引数の解釈コードが面倒です。 それはもう、あらゆる要素をソースコードに書き込んで毎回ソースコード上の値を書き換えてビルドして使うほどに。

というわけで、引数を扱うコードをメモしておきます。まだいろいろ不完全なので今後いじっていきたいコードのちょっとバージョンアップ版コードです。
今回は引数を格納するオブジェクトを record にできるようにしました。

■ 引数オブジェクト用インタフェース

今回はインスタンスを生成する前にパラメーターに対応するプロパティの名前がわからなければならないので、パラメーター名のリストを static にしてみました。 引数を格納するクラスには次のインタフェースを実装してもらいます。

public interface IArguments
{
    static IEnumerable<string> ParameterNames { get; } = new string[] { };
}

実装例

record struct Arguments(string? Aparam, string? Bparam, string? A, string? B, string? C, bool D, bool E) : IArguments
{
     public static IEnumerable<string> ParameterNames => new[] { nameof(Aparam), nameof(Bparam) };
}

■ 引数を解釈するコード

一つのメソッドの中にひたすら書き続けたので、かなり読みにくいので今後書き換えていきたいです。
ただ読むのは大変ですが、コピペで使うには楽かもしれません。

public static class ArgumentsBuilder
{
    public static T Parse<T>(string[] args) where T : IArguments, new()
    {
        var argumentsType = typeof(T);
        List<string> parameters = new();
        Dictionary<PropertyInfo, string> options = new();
        Dictionary<PropertyInfo, bool> switchs = new();
        PropertyInfo? optionProperty = default;

        foreach (var arg in args.Select(arg => arg ?? string.Empty))
        {
            {
                var pre = arg.First();
                if (new[] { '-', '/' }.Contains(pre))
                {
                    var optionName = new string(arg.SkipWhile(c => c == pre).ToArray());
                    optionProperty = argumentsType.GetProperty(optionName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                    if (optionProperty == null) continue;

                    if (optionProperty.PropertyType == typeof(bool))
                    {
                        switchs.Add(optionProperty, true);
                        optionProperty = null;
                        continue;
                    }

                    options.Add(optionProperty, "");
                    continue;
                }
            }

            if (optionProperty == null)
            {
                parameters.Add(arg);
                continue;
            }

            options[optionProperty] = arg;
            optionProperty = null;
        }

        var parameterNames = ((IEnumerable<string>)(argumentsType.GetProperty(nameof(IArguments.ParameterNames))?.GetValue(null) ?? new string[0])).ToList();
        foreach (var o in options.Where(o => parameterNames.Contains(o.Key.Name)).ToArray()) options.Remove(o.Key);
        foreach (var o in switchs.Where(o => parameterNames.Contains(o.Key.Name)).ToArray()) options.Remove(o.Key);

        var constractor = argumentsType.GetConstructors().OrderByDescending(c => c.GetParameters().Length).FirstOrDefault();
        T arguments = constractor switch
        {
            null => new(),
            _ => ((Func<T>)(() =>
            {
                var constractorParameterLength = constractor.GetParameters().Length;
                var constractorParameters = constractor.GetParameters();
                var constractorParameterValues = constractorParameters.Select(p =>
                {
                    var name = p.Name ?? string.Empty;
                    if (parameterNames.Contains(name))
                    {
                        parameterNames.Remove(name);
                        var value = parameters.FirstOrDefault();
                        if (parameters.Count > 0) parameters.RemoveAt(0);
                        return value;
                    }

                    {
                        var option = options.FirstOrDefault(o => o.Key.Name == name);
                        if (!string.IsNullOrWhiteSpace(option.Key?.Name))
                        {
                            options.Remove(option.Key);
                            return option.Value;
                        }
                    }

                    {
                        var @switch = switchs.FirstOrDefault(o => o.Key.Name == name);
                        if (!string.IsNullOrWhiteSpace(@switch.Key?.Name))
                        {
                            switchs.Remove(@switch.Key);
                            return @switch.Value;
                        }
                    }

                    return (object?)null;
                }).ToArray();

                return (T)constractor.Invoke(constractorParameterValues);
            }))(),
        };

        foreach (var parameter in Enumerable.Zip(parameterNames, parameters)) argumentsType.GetProperty(parameter.First)?.SetValue(arguments, parameter.Second);
        foreach (var option in options) option.Key.SetValue(arguments, option.Value);
        foreach (var @switch in switchs) @switch.Key.SetValue(arguments, @switch.Value);

        return arguments;
    }
}

■ 使う方

<実行ファイル名> aparam -a aopt -d -b bopt bparam /c copt
var parsed = ArgumentsBuilder.Parse<Arguments>(args);

// パラメーター
parsed.Aparam; // aparam
parsed.Bparam; // bparam;

// オプション
parsed.A; // aopt
parsed.B; // bopt
parsed.C; // copt

// スイッチ
parsed.D; // true
parsed.E; // false

雑に作るコンソールアプリ用のコピペ元としてはこんな感じでいいでしょうかね。

コマンドライン引数を雑に扱う (2)

以前にコマンドライン引数を雑に扱う記事を書きました。

コンソールアプリ、ちょっとした処理を行う際に雑に作ってしまうのが結構いいのですが、何気にコマンドライン引数の解釈コードが面倒です。 それはもう、あらゆる要素をソースコードに書き込んで毎回ソースコード上の値を書き換えてビルドして使うほどに。

というわけで、引数を扱うコードをメモしておきます。まだいろいろ不完全なので今後いじっていきたいコードのちょっとバージョンアップ版コードです。

■ 今回使う言葉

今回、-/ で始まる引数をオプションまたはスイッチと呼びます。

オプション

-/ で始まる引数の次の引数が対応する値となる場合、オプション。

スイッチ

-/ の後に値を取らない (実体としては bool 型プロパティに true をセットする) 場合、スイッチと呼びます。

パラメーター

-/ で始まる引数でもその値でもないものをパラメーターと呼びます。

よくわからないと思うのでこの記事の最後の実例を見てください。

■ 引数オブジェクト用インタフェース

今回は引数を格納するクラスをユーザーが作って、そのオブジェクトに引数の値を詰めるようにしてみました。
引数を格納するクラスには次のインタフェースを実装してもらいます。

public interface IArguments
{
    IEnumerable<string> ParameterNames { get; }
}

実装例

class Arguments :IArguments
{
    // パラメータを頭から格納するプロパティの名前リスト(リストの先頭から順にその名前を持つプロパティに値をセットしていく)
    public IEnumerable<string> ParameterNames => new[] { nameof(Aparam), nameof(Bparam) };

    public string? Aparam { get; set; }
    public string? Bparam { get; set; }
    public string? A { get; set; }
    public string? B { get; set; }
    public string? C { get; set; }
    public bool D { get; set; }
    public bool E { get; set; }
}

■ 引数を解釈するコード

public static T Parse<T>(string[] args) where T : IArguments, new()
{
    var argumentsType = typeof(T);
    List<string> parameters = new();
    Dictionary<PropertyInfo, string> options = new();
    Dictionary<PropertyInfo, bool> switchs = new();
    PropertyInfo? optionProperty = default;

    foreach (var arg in args.Select(arg => arg ?? string.Empty))
    {
        {
            var pre = arg.First();
            if (new[] { '-', '/' }.Contains(pre))
            {
                var optionName = new string(arg.SkipWhile(c => c == pre).ToArray());
                optionProperty = argumentsType.GetProperty(optionName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                if (optionProperty == null) continue;
                if (optionProperty.PropertyType == typeof(bool))
                {
                    switchs.Add(optionProperty, true);
                    optionProperty = null;
                    continue;
                }
                options.Add(optionProperty, "");
                continue;
            }
        }
 
        if (optionProperty == null)
        {
            parameters.Add(arg);
            continue;
        }
 
        options[optionProperty] = arg;
        optionProperty = null;
    }
 
    var arguments = new T();
    foreach (var parameter in Enumerable.Zip(arguments.ParameterNames, parameters)) argumentsType.GetProperty(parameter.First)?.SetValue(arguments, parameter.Second);
    foreach (var option in options) option.Key.SetValue(arguments, option.Value);
    foreach (var @switch in switchs) @switch.Key.SetValue(arguments, @switch.Value);
 
    return arguments;
}

■ 使う方

<実行ファイル名> aparam -a aopt -d -b bopt bparam /c copt
var parsed = ArgumentsBuilder.Parse<Arguments>(args);

// パラメーター
parsed.Aparam; // aparam
parsed.Bparam; // bparam;

// オプション
parsed.A; // aopt
parsed.B; // bopt
parsed.C; // copt

// スイッチ
parsed.D; // true
parsed.E; // false

雑に作るコンソールアプリ用のコピペ元としてはこんな感じでいいでしょうかね。

ただ、このコード少し言語仕様的に古いと思うのでまたアップデートしたいです。

大文字小文字を区別せずにプロパティ情報を取得する

PropertyInfo を取得する際、大文字小文字を区別して取得したい方は非常にまれだと思います。
大多数の大文字小文字を区別せずに PropertyInfo を取得したい方向けのお話です。ちなみに私も大文字小文字を区別せずに PropertyInfo を取得したい派です。

■ BindingFlags.IgnoreCase

BindingFlags.IgnoreCase が大文字小文字を区別せずに取得するフラグです。しかし、次のコードは期待通りに動作しません。

var property = type.GetProperty(propertyName, BindingFlags.IgnoreCase);
// property はnull。PropertyInfo が取得できていない。

期待通りでないこの挙動が仕様です。

■ 期待通りの動作をするコード

var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);

何度も失敗すると思います、というか私が何度も失敗しています。次失敗したときに検索してコピペしましょう。