【C#】指数表記の文字列をdecimal型で扱う

指数表記文字列を decimal 型に変換するための Parse メソッドを使うとエラーが発生する問題の対方法です。

// 指数表記の文字列を変換するとエラーになる
string str = "-1.2345678E-07";
var value = decimal.Parse(str);
// System.FormatException: '入力文字列の形式が正しくありません。'

以下のように指定すると変換できるようになります。

// 第2引数に形式を指定する
var value = decimal.Parse(str, System.Globalization.NumberStyles.Float);

第2引数の NumberStyles に「NumberStyles.AllowExponent | NumberStyles.Float」みたいな指定は不要です。

また、小数点にカンマを使う地域(主にEU圏)でピリオドで小数点を表してる文字列を扱うと問題を起こすのでそれを避ける場合は、以下の通り第3引数を指定します。

using System.Globalization;
//...
var value = decimal.Parse(str, NumberStyles.Float, CultureInfo.InvariantCulture);

InvariantCulture を指定すると地域の表記の違いを無視して同じ動作を行います。小数点の表記はピリオドとカンマの2種類があるため指定が必要な場合がります。

InvariantCulture の小数点の表記はピリオドです。このためカンマ表記の地域で Parse などの変換メソッドで文字列を扱う場合、対象文字列に含まれる小数点はピリオド扱いとなります。

(逆にEU圏で数値を ToString するときにInvariantCultureを指定しないで文字列にすると 1,234...みたいなカンマ表記になって読み取る際に問題が起きることがあるので 数値⇔文字列 の相互変換の実装は注意が必要です)

確認環境

本記事は以下環境で確認しました。

  • .NET Framework 4.7.2
  • .NET 8.0
  • Visual Studio 2022

【C#】Traceの出力先をファイルに変更する

Traceの出力先をファイルにしたりカスタムクラス用いて任意の出力先を設定する方法の紹介です。

確認環境

  • Windows11
  • VisualStudio2022
  • .NET 7.0

.NET 7.0 で確認しましたがどの環境でも同じです。

Traceの出力先の変更方法

出力先の変更方法は Trace.ListenersTraceListener を継承したクラスを指定します。

Trace.Listeners.Add(TraceListener);

トレースに出力が不要な場合は Listeners を削除してから追加します。

// 出力先をクリアする
Trace.Listeners.Clear();
// 新しい出力先を指定する
Trace.Listeners.Add(TraceListener);

標準で TraceListener を継承したクラスがあるため併せて紹介します。

  • DefaultTraceListener: デバッグウインドウに内容を出力する
  • ConsoleTraceListener: コンソールに出力する
  • TextWriterTraceListener: テキストに出力する

出力先をファイルに変更する

単純にファイルに出力するだけであれば TraceListener を継承した TextWriterTraceListener が標準であるのでこれを使用します。

var text = new TextWriterTraceListener(@"d:\sample.txt");
Trace.Listeners.Add(text);

カスタム出力先を指定する

以下のように TraceListener を継承したクラスを作成できます。

// Traceへの出力をファイルへ書き出すためのクラス
public class FileListner : TraceListener
{
    // 出力するファイルパス
    private string _path;

    // 出力するファイルパスを指定してオブジェクトを初期化する
    public FileListner(string filePath) => _path = filePath;

    // 基底クラスのメソッドの実装
    public override void Write(string message)
    {
        using (var sw = File.AppendText(_path))
        {
            sw.Write(message);
        }
    }
    public override void WriteLine(string message)
    {
        using (var sw = File.AppendText(_path))
        {
            sw.WriteLine(message);
        }
    }
}

使用方法は以下の通りです。

var text = new FileListner(@"d:\sample.txt");
Trace.Listeners.Add(text);

【C#】ローカルで作成した待機ハンドルをサービスで使用する

ユーザーがログインしているアカウント上で起動しているソフトで生成した待機ハンドルを同じPC上のサービス (LocalSystem = NT AUTHORITY\SYSTEM) 上で利用する方法の紹介です。

通常、クライアントが作成した名前付き待機ハンドル (EventWaitHandle) をサービス上で OpenExisting メソッドで取得しようとすると以下のような例外が発生します。

System.Threading.WaitHandleCannotBeOpenedException:

指定された名前のハンドルは存在しません。

これを回避するためにはユーザー側で EventWaitHandle を作成するときにハンドル名の先頭に "Global\" を追加します(たったこれだけで待機ハンドル共有できるようになります)

補足:

AutoResetEventManualResetEvent は名前が指定できない(同一プロセス内の利用限定) のため名前を指定できる EventWaitHandle を使用します。

実装例

// クライアント側

public void Client()
{
    // 待機ハンドルの名前の先頭にGlobal\を指定する
    string name = $@"Global\{Guid.NewGuid().ToString()}";
    
    using (var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset, name))
    {
        Service(name); // サービスに処理要求を投げる

        // サービス側でシグナル状態にされる or タイムアウトするまで待機する
        if (waitHandle.WaitOne(5000)
        {
            Console.WriteLine("OK");
        }
        else
        {
            Console.WriteLine("Timeout"); // 5秒以内に解除されなかった場合
        }
    }
}

サービス側の実装は以下の通りです。

Global\ プレフィックスが付いていればそのまま OpenExisting や TryOpenExisting でハンドルが取得できます。

// サービス側

public void Servicec(string name)d
{
    Task.Run(() =>
    {
        try
        {
            var isOk = EventWaitHandle.TryOpenExisting(name, out var result);
            if (isOk)
            {
                result.Set();
            }
            else
            {
                // もし取れない場合クライアント側でタイムアウトを待ち
            }
        }
    }
}

【C#】ファイルを別の場所に書き出してから保存する

既存のファイルに内容を書きこむ時に、直接対象のファイルを開いて書き込みを行うとアプリが強制終了するなどでストリームが異常終了するとファイルの内容が破損する場合があります。この問題を避けるためには以下のアプローチが必要です。

  • 直接ファイルを開かない
  • 別の場所に書き出してからファイルを保存先に移動する

ですが、この処理を毎回書くのは多少面倒なので、簡単に上記動作を実行できる Utility を作成してみました。

using System;
using System.IO;

public static class FileUtility
{
    // ファイルをいったん一時領域に出力してから保存する
    public static void SaveNew(string savePath, Action<string> saveAction)
    {
        string tempPath = "";
        try
        {
            tempPath = GetUniquePath();
            saveAction(tempPath);
            Microsoft.VisualBasic.FileIO.FileSystem.MoveFile(tempPath, savePath, true);
        }
        finally
        {
            if (File.Exists(tempPath)) // ゴミが残らないようにする
            {
                File.Delete(tempPath);
            }
        }
    }

    // 既存のファイルに内容を追記する
    // ** 何百MBもあるファイルに対してこのメソッドを使うとかなり重い
    public static void Append(string filePath, Action<string> appendAction)
    {
        if (!File.Exists(filePath))
        {
            SaveNew(filePath, appendAction);
        }
        else
        {
            string tempPath = "";
            try
            {
                tempPath = GetUniquePath();
                File.Copy(filePath, tempPath, true);
                appendAction(tempPath);
                Microsoft.VisualBasic.FileIO.FileSystem.MoveFile(tempPath, filePath, true);
            }
            finally
            {
                if (File.Exists(tempPath))
                {
                    File.Delete(tempPath);
                }
            }
        }
    }

    // ユニークな一時ファイルパスを取得する
    public static string GetUniquePath()
    {
        string filePath;
        do
        {
            filePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".tmp");
        }
        while (File.Exists(filePath)); // 使用可能なパスを取得できるまで繰り返す
        return filePath;
    }
}

使い方は以下の通りです。

// Program.cs

using System;
using System.IO;
using System.Text;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");

        // ラムダ式で処理を指定
        FileUtility.Save(@"d:\sample1.txt",
            tmpPath =>
            {
                var sw = new StreamWriter(tmpPath, false, Encoding.UTF8);
                sw.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));
            });

        // メソッドを指定(★推奨)
        FileUtility.Save(@"d:\sample2.txt", Save);
    }

    // ファイルパスを受け取ってそこに内容を保存する処理
    private static void Save(string filePath)
    {
        var sw = new StreamWriter(filePath, false, Encoding.UTF8);
        sw.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));
    }
}

これでファイルを開きっぱなしでアプリがクラッシュしてデータが全部消えるという状態を少しは避けることができると思います。

【C#】UTF-8のBOMあり/BOMなしの指定

UTF-8はBOMの有無があり時と場合によって適切に選択する必要があります。

// BOMあり
Encoding bom = System.Text.Encoding.UTF8;
// BOMなし
Encoding withoutBom = System.Text.UTF8Encoding(false);

取得方法に対称性が無いのが気になるので Utility 化してみました。

// UTF8.cs

using System.Text;

public static class UTF8
{
    static UTF8Encoding? _utf8withoutBom;

    // UTF8エンコードを取得します。
    // true: BOMあり / false: BOMなし
    public static Encoding GetEncoding(bool useBom)
    {
        return useBom
            ? Encoding.UTF8
            : _utf8withoutBom ??= new UTF8Encoding(false);
    }
}

// ★使い方 - - - - -

// BOMあり
Encoding bom = UTF8.GetEncoding(true);
// BOMなし
Encoding withoutBom = UTF8.GetEncoding(false);

【C#】コマンド実行用のバッファリングキューを実装する

バッファーがいっぱいになるまではデータをバッファリングをしながらバックグランドで1つずつ順番にデータを処理して、バッファーがいっぱいになったら空きができるまで待機となるコマンド実行用のデータキューイングクラスの実装例の紹介です。

確認環境

  • .NET Framwork 4.8.1
  • C# 7.3
  • Visual Studio 2022

IDE上のデバッグ実行で動作を確認。

少し古めの環境で作成したので .NET のほぼすべての環境で使用できると思います。

使い方

先に使い方を紹介したいと思います。このクラスの使用方法は、Init で初期化した後、MaxCapacity でバッファー最大値を指定、ProcItem に要素の処理方法を指定してから EnqueueAsync でデータをバッファリングしながら処理するように指示をします。

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

internal class AppMain
{
    static readonly BufferdQueue<int> _queue = new BufferdQueue<int>();

    private static void Main(string[] args)
    {
        // 使う前に初期化する
        _queue.Init();

        // バッファーの最大容量を50に変更
        _queue.MaxCapacity = 50;

        // バッファリングしたデータに対する処理の登録
        _queue.ProcItem += item =>
        {
            Thread.Sleep(10); // ちょっと遅い処理
            Trace.WriteLine($"Proc={item}");
        };

        RunFireAndforget();

        Console.ReadLine();

        // バッファリング処理を終了するときに呼び出す
        _queue.Terminate();
    }

    // 非同期で処理を実行する
    private static void RunFireAndforget()
    {
        Task.Run(async () =>
        {
            for (int i = 0; i < 500; i++)
            {
                // キューが一杯になるまではどんどんバッファリングされていく
                // いっぱいになると空きができるまで待機になる
                await _queue.EnqueueAsync(i);
            }
        });
    }
}

実装コード

実装コードは以下の通りです。2つの待機ハンドルを使ってバックグラウンドのスレッドの進行制御とユーザーがデータを追加したときの進行制御を行っています。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

// ユーザー側の処理速度のほうがバックエンドの処理速度より速いときに
// いちいちメソッドをブロックキングで待機すると
// 全体の処理速度が低下するのをバッファリングすることで緩和する事を目的としたクラス
//
// メモ:
// バッファーがいっぱいになると同期実行に切り替わって実行速度が低下するので
// 処理に局所性があるもに対して十分なキャパシティを確保して使用すること
public class BufferdQueue<T>
{
    readonly Queue<T> _queue = new Queue<T>();
    // キューの排他アクセス用
    readonly object _lockObject = new object();
    // バックエンドのスレッド用の待機ハンドル
    readonly EventWaitHandle _ProcActionHandle 
        = new EventWaitHandle(false, EventResetMode.ManualReset);
    // フロントのキュー用の待機ハンドル
    readonly EventWaitHandle _EnqueueHandle
        = new EventWaitHandle(true, EventResetMode.ManualReset);

    // Queueに蓄積できる最大容量
    int _maxCapacity = 1000;
    // Queueがバッファリングを再開する閾値
    int _threthold;
    // 終了要求を受け付けたかどうか
    // true: 受け付けた / false: それ以外
    bool _isTerminate;
    // バックエンドのバッファを処理するスレッド
    Thread _procThread;
    // 中断を受け付けたかどうかのフラグ
    // true: 受け付けた / false: それ以外
    bool _isAbort;

    // キューの最大容量
    public int MaxCapacity
    {
        get => _maxCapacity;
        set
        {
            if (_maxCapacity < 16) // あまりに小さい数値は受け付けない
            {
                throw new InvalidOperationException("Less than 16 cannot be specified.");
            }
            lock (_lockObject)
            {
                _threthold = value - 1;
                _maxCapacity = value;
            }
        }
    }

    // キューから取り出したデータに対する処理を行う時に発生する
    public event Action<T> ProcItem;

    // Constructors - - - - - - - - - -

    public BufferdQueue()
    {
        _threthold = _maxCapacity - 1;
    }

    // Public Methods - - - - - - - - - -

    // 使う前に呼び出すこと
    public void Init()
    {
        _procThread = new Thread(new ThreadStart(ProcAction))
        {
            IsBackground = true
        };
        _procThread.Start();
    }

    // バッファーに余裕があるときはitemをキューイングして即座に終了
    // or
    // バッファーがいっぱいならバッファーに空きが出るまでブロックして待機する
    public void Enqueue(T item)
    {
        CheckInitializedIfThrowException();

        lock (_lockObject)
        {
            _queue.Enqueue(item);
            _ProcActionHandle.Set();
            if (_queue.Count > MaxCapacity)
            {
                _EnqueueHandle.Reset();
                //Trace.WriteLine("Max capacity1");
            }
        }
        _EnqueueHandle.WaitOne(); // バックグラウンドで処理がはけるまで待機
    }

    public async Task EnqueueAsync(T item)
    {
        CheckInitializedIfThrowException();

        await Task.Run(() =>
        {
            lock (_lockObject)
            {
                _queue.Enqueue(item);
                _ProcActionHandle.Set();
                if (_queue.Count >= MaxCapacity)
                {
                    _EnqueueHandle.Reset();
                }
            }
            _EnqueueHandle.WaitOne(); // バックグラウンドで処理がはけるまで待機
        });
    }

    public void Terminate()
    {
        CheckInitializedIfThrowException();
        _isAbort = true;
        _EnqueueHandle.Set();

        while (!_isTerminate)
        {
            Thread.Sleep(1);
        }
    }

    // Private Methods - - - - - - - - - -

    private void CheckInitializedIfThrowException()
    {
        if (_procThread == null)
        {
            throw new InvalidOperationException("Object not initialized.");
        }
    }

    private void ProcAction()
    {
        try
        {
            //Trace.WriteLine("ProcAction Start");

            while (true)
            {
                _ProcActionHandle.WaitOne();

                if (_isAbort)
                {
                    lock (_lockObject)
                    {
                        _queue.Clear();
                    }
                    break;
                }

                T item;
                lock (_lockObject)
                {
                    if (_queue.Count == 0)
                    {
                        _ProcActionHandle.Reset();
                        continue;
                    }
                    else
                    {
                        item = _queue.Dequeue();
                    }

                    if (_queue.Count <= _threthold)
                    {
                        _EnqueueHandle.Set();
                        //Trace.WriteLine($"Free capacity={_queue.Count}");
                    }
                }
                
                try
                {
                    ProcItem?.Invoke(item);
                }
                catch (Exception) { }
            }
            _queue.Clear(); // abortで抜けたら要素は全て解放
        }
        finally
        {
            _isTerminate = true;
            //Trace.WriteLine("ProcAction End");
        }
    }
}

【C#】一定時間経過すると削除されるリストの実装

何の役に立つかはわかりませんが、一定時間経過したら削除されるリストを実装しててみました。

確認環境

  • .NET 6
  • VisualStudio 2022

実装コード

規定では Add(...) した後に、5秒以内に TryGetItemAndRemove() でデータを取り出されなければバックグラウンドのタイマー処理でデータが消去されます。

自動で削除された要素は AutoRemoved イベントで通知されるので後処理が必要ならイベントを購読します。

// 一定時間経過するとデータが消去されるリスト
public class ExpirationTimeItemHolder<TKey, TValue> : IDisposable  
    where TKey : IEquatable<TKey> 
    where TValue : class
{
    readonly System.Timers.Timer _timer;

    readonly List<ItemBug> _list = new List<ItemBug>();

    readonly object _lockObj = new object();
    
    private bool _isDisposed;

    // データの保持期間
    public TimeSpan HoldTime { get; set; } = TimeSpan.FromSeconds(5);

    // 保持期間が過ぎて自動で要素が削除された時に発生します
    public event Action<TValue> AutoRemoved;

    public ExpirationTimeItemHolder()
    {
        _timer = new System.Timers.Timer(100);
        _timer.Elapsed += OnTimerElapsed;
    }

    ~ExpirationTimeItemHolder() => Dispose(false);

    // 一定時間経過したら削除するタイマーハンドラー
    private void OnTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        var removeList = new List<ItemBug>();
        DateTime now = DateTime.Now;
        lock (_lockObj)
        {
            for (int i = 0; i < _list.Count; i++)
            {
                var item = _list[i];
                TimeSpan diff = now - item.Time;
                if (diff > HoldTime)
                {
                    removeList.Add(item);
                    Trace.WriteLine($"[diff] {diff.TotalMilliseconds}ms");
                }
            }

            for (int i = 0; i < removeList.Count; i++)
            {
                var removeItem = removeList[i];
                _list.Remove(removeItem);
                AutoRemoved?.Invoke(removeItem.Value);
                Trace.WriteLine($"[timer remove] {removeItem.Key}");
            }

            if (_list.Count == 0)
            {
                _timer.Stop();
                Trace.WriteLine("timer stop");
            }
        }
    }

    // データを追加する
    public void Add(TKey key, TValue value)
    {
        lock (_lockObj)
        {
            _list.Add(new ItemBug(key, DateTime.Now, value));
            Trace.WriteLine($"[add] {key}");
            if (!_timer.Enabled)
            {
                _timer.Start();
            }
        }
    }

    // データを取得してリストから削除する
    public bool TryGetItemAndRemove(TKey key, out TValue resultItem)
    {
        lock (_lockObj)
        {
            resultItem = default;
            ItemBug tempBug = null;

            for (int i = 0; i < _list.Count; i++)
            {
                var item = _list[i];
                if (key.Equals(item.Key))
                {
                    tempBug = item;
                    break;
                }
            }

            if (tempBug != null)
            {
                _list.Remove(tempBug);
                Trace.WriteLine($"[pop] {tempBug.Key}");
            }
            
            if (tempBug != null) resultItem = tempBug.Value;
            return resultItem != null;
        }
    }

    // InnerTypes

    private class ItemBug
    {
        public readonly TKey Key;
        public readonly DateTime Time;
        public readonly TValue Value;
        public ItemBug(TKey key, DateTime time, TValue value)
        {
            Key = key;
            Time = time;
            Value = value;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_isDisposed)
        {
            if (disposing)
            {
                lock (_lockObj)
                {
                    using (_timer) { }
                    AutoRemoved = null;
                }
            }
            _isDisposed = true;
        }
    }
}

【C#】Fisher-Yatesを使って配列/リストをシャッフルする

Fisher-Yates(フィッシャーイェーツ)というアルゴリズムを使って配列やリストを並び替えたいと思います。

アルゴリズムの考え方ですが N 個の要素数があったとして

  • 一番最後の要素 (N) をそれ以外の前方の要素とランダムに交換する
  • 一番最後から -1個目を前方の要素とランダムに交換する
  • -2個目を前方の要素とランダムに交換する
  • N-1個目まで繰り返す

と、後ろから前方に範囲を狭めながら要素を好感していき処理が終わると配列の内容が全てシャッフルされるアルゴリズムです。配列に対してこの処理を実行すると内容が不可逆にシャッフルされるのでそれだけ注意しましょう。

処理回数は O(N) だと思うんので計算量はかなり少ないほうだと思います。

実装内容

通常の .NET 向けの処理と Unity 向けの処理を #if で切り替えています。

両環境このままコピペすれば動くはずです。

public static class RandomUtil
{
    //
    // Unity 向けの実装
    // 

#if UNITY_5_3_OR_NEWER

    /// <summary>
    /// Fisher-Yates(フィッシャー・イェーツ) アルゴリズムでコレクションをシャッフルします。
    /// </summary>
    /// <remarks>
    /// 指定した配列の順序が不可逆に変更されます。
    /// </remarks>
    public static void Shuffle<T>(IList<T> collection)
    {
        int n = collection.Count;
        for (int i = n - 1; i > 0; i--)
        {
            int j = UnityEngine.Random.Range(0, i + 1);
            T tmp = collection[i];
            collection[i] = collection[j];
            collection[j] = tmp;
        }
    }

    /// <summary>
    /// Fisher-Yates(フィッシャー・イェーツ) アルゴリズムでコレクションをシャッフルします。
    /// </summary>
    /// <remarks>
    /// 指定した配列の順序が不可逆に変更されます。
    /// </remarks>
    public static void Shiffle<T>(T[] array)
    {
        int n = array.Length;
        for (int i = n - 1; i > 0; i--)
        {
            int j = UnityEngine.Random.Range(0, i + 1);
            T tmp = array[i];
            array[i] = array[j];
            array[j] = tmp;
        }
    }
#else
    //
    // .NET 向けの実装
    // 

    static readonly Random _rand = new();

    /// <summary>
    /// Fisher-Yates(フィッシャー・イェーツ) アルゴリズムでコレクションをシャッフルします。
    /// </summary>
    /// <remarks>
    /// 指定した配列の順序が不可逆に変更されます。
    /// </remarks>
    public static void Shiffle<T>(IList<T> collection)
    {
        int n = collection.Count;
        for (int i = n - 1; i > 0; i--)
        {
            int j = _rand.Next(0, i + 1);
            T tmp = collection[i];
            collection[i] = collection[j];
            collection[j] = tmp;
        }
    }

    /// <summary>
    /// Fisher-Yates(フィッシャー・イェーツ) アルゴリズムでコレクションをシャッフルします。
    /// </summary>
    /// <remarks>
    /// 指定した配列の順序が不可逆に変更されます。
    /// </remarks>
    public static void Shiffle<T>(T[] array)
    {
        int n = array.Length;
        for (int i = n - 1; i > 0; i--)
        {
            int j = _rand.Next(0, i + 1);
            T tmp = array[i];
            array[i] = array[j];
            array[j] = tmp;
        }
    }
}
#endif

ちなみに seed 値はできないので再現性は考慮していません。

【C#】オープン中のファイルの内容を読み取る

何度も何度も調べなおしてるので自分用のメモです。

他のプロセスが開いているファイルを開こうとすると以下のエラーが発生する。

System.IO.IOException: 
別のプロセスで使用されているため、プロセスはファイル 'xxxx' にアクセスできません。

以下のように書けば読み取れるようにななるが、確実に読み取れる訳でははない。

string filePath = "c:\hoge\hoge.txt";
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var sr = new StreamReader(fs))
{
    // 読み取れる
}

これで読み取れなかった諦めること。

【C#/Unity】重み付き抽選機能を実装する

よくある重み付きの抽選機能の実装例の紹介です。

重み付き抽選とは要素ごとに選ばれる確率が違う抽選方法です。

例えば以下のように各々確率が違うものをランダムで選びます。

  • Aは50%
  • Bは25%
  • Cは20%
  • Dは5%

確認環境

  • Unity 2022.3.5f1
  • VisualStudio 2022

Editor 上のみで動作を確認

使い方

まずは使い方です

// Sample.cs

// フルーツを重み付き抽選で選ぶ
public void Select()
{
    // 抽選確立のリストを作成する
    List<LotteryItem<Fruit>> items = new();
    items.Add(new LotteryItem<Fruit>(Fruit.Apple, 50.5f));    // 50.5%
    items.Add(new LotteryItem<Fruit>(Fruit.Banana, 25.5f));   // 25.5%
    items.Add(new LotteryItem<Fruit>(Fruit.Orange, 15.0f));   // 15.0%
    items.Add(new LotteryItem<Fruit>(Fruit.Grape, 5.0f));     // 5.0%
    items.Add(new LotteryItem<Fruit>(Fruit.Pineapple, 4.0f)); // 4.0%

    // 抽選する
    Fruit result = RandomUtil.SelectOne(items);
}

public enum Fruit 
{
    Apple, Banana, Orange, Grape, Pineapple
}

実装コード

合計で 100になるようにサンプルを作りましたが重みは100でなくても大丈夫です。100より多くても少なくてもその割合で要素が抽選されます。

// RandomUtil.cs

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public static class RandomUtil
{
    // 重み付き抽選を行う(配列用)
    public static T SelectOne<T>(LotteryItem<T>[] list)
    {
        float total = 0;
        for (int i = 0; i < list.Length; i++)
        {
            total += list[i].Weight;
        }

        float value = Random.Range(0, total);
        for (int i = 0; i < list.Length; i++)
        {
            value -= list[i].Weight;
            if (value <= 0) return list[i].Value;
        }

        return default;
    }

    // 重み付き抽選を行う(リスト用:ちょっと動作が遅い)
    public static T SelectOne<T>(List<LotteryItem<T>> list)
    {
        float total = 0;
        for (int i = 0; i < list.Count; i++)
        {
            total += list[i].Weight;
        }

        float value = Random.Range(0, total);
        for (int i = 0; i < list.Count; i++)
        {
            value -= list[i].Weight;
            if (value <= 0) return list[i].Value;
        }

        return default;
    }
}

[System.Serializable]
public readonly struct LotteryItem<T>
{
    public readonly T Value;
    public readonly float Weight;

    public LotteryItem(T value, float weight)
    {
        Value = value;
        Weight = weight;
    }
}

余談ですが、この実装は seed の指定がないため起動ごとに適当な seed が使用されるので再現性が無いです。もしかするとデバッグ時に困るケースがあります。

もし、必要なら UnityEngine.Random.state をどこかにもってそれを指定する仕組みが必要です。その場合 static メソッドではなくクラスにしてインスタンスの中に Random.State を持つなど対応しましょう。

【C#】ListをAsSpanしたい

ハック的な手法で List に AsSpan の拡張メソッドをは生やすことができますが、.NET5からは標準ライブラリでサポートされたので両対応してみます。

public static class ListExtensions
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Span<T> AsSpan<T>(this List<T> self)
    {
#if NET5_0_OR_GREATER
        return System.Runtime.InteropServices.CollectionsMarshal.AsSpan(self);
#else
        // Hacked!!!!
        return Unsafe.As<ListDummy<T>>(self).Items.AsSpan(0, self.Count);
#endif
    }

    private class ListDummy<T> { internal T[] Items; }
}

// 使い方
List<int> list = new() { 1, 2, 3, 4, 5 };
Span<int> span = list.AsSpan();

まぁもう .NET5 以前環境はあまり無いと思いますが、、、

【C#】アプリを多重起動しないようにする

Mutex を取得して新規に作成できれば新規の起動、取れなかったら 2つめの起動という感じに判断できます。

string key = "application_name";
using (var mutex = new Mutex(true, key, out bool createdNew))
{
    if (!createdNew)
    {
        return; // 多重起動になる
    }
    else
    {
        // 多重起動ではない → ここにメインの処理を記述する
        mutex.ReleaseMutex();
    }
}

定型的な処理なので Utility 化したいと思います。

// MultiStartupUtil.cs

using System;
using System.Threading;

public static class MultiStartupUtil
{
    public static void SingleStartupContext(string key, Action action)
    {
        using (var mutex = new Mutex(true, key, out bool createdNew))
        {
            if (!createdNew)
            {
                return;
            }
            action?.Invoke();
            mutex.ReleaseMutex();
        }
    }
}

使い方は以下の通りです。

// Program.cs

using System;
using System.Windows.Forms;

internal static class Program
{
    [STAThread]
    static void Main()
    {
        // 多重起動防止用のメソッドにキーとメイン処理を渡す
        MultiStartupUtil.SingleStartupContext("application_name", _Main);
    }

    // メイン処理をこっちに引っ越す
    private static void _Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        using (FormTaskTray formTaskTray = new FormTaskTray())
        {
            Application.Run();
        }
    }
}