【C#】2,8,10,16進数文字列と数値の相互変換方法まとめ

数字と文字列にはいろいろ変換方法があるのですが、一覧的に確認できると便利かと思いまとめてみました。

他にもいろいろやり方はありますがこれさえ覚えておけば問題ないと思います。

数値 → 2,8,10,16進数文字列 に変換

数値からN進数の文字列に変換したい場合、各基本型についている ToString() メソッドか Convert.ToString() メソッドを使用します。

元の型 変換先 やり方(1) やり方(2)
数値 2進数の文字列 Convert.ToString ( num , 2 );
数値 8進数の文字列 Convert.ToString ( val , 8 );
数値 10進数の文字列 num.ToString ( num ); Convert.ToString ( num );
数値 16進数の文字列 num.ToString ( "x" ); Convert.ToString ( num , 16 );

使用例は以下の通りです。

public static void Foo()
{
    int inum = 3130;

    // 整数値 → 2進数の文字列に変換
    string str2 = Convert.ToString(inum, 2);
    // > str2 = 110000111010

    // 整数値 → 8進数の文字列に変換
    string str8 = Convert.ToString(inum, 8);
    // > str8 = 6072

    // 整数値 → 10進数の文字列に変換
    string str101 = inum.ToString();
    string str102 = Convert.ToString(inum);
    // > str101 = 3130, str102 = 3130

    // 整数値 → 16進数の文字列に変換
    string str161 = inum.ToString("X"); // "x"はアルファベット部分が小文字, "X"は大文字となる
    string str162 = Convert.ToString(inum, 16);
    // > str161 = C3A, str162 = c3a
}

2,8,10,16進数文字列 → 数値 に変換

Convert クラスにある ToXxxx() メソッドで変換できます。例えば int 型に変換したいときは Convert.ToInt32() メソッドを使用します。

元の型 変換先 やり方
2進数の文字列 int Convert.ToInt32 ( "110000111010" , 2 );
8進数の文字列 int Convert.ToInt32 ( "6072" , 8 );
10進数の文字列 int Convert.ToInt32 ( "3130" );
16進数の文字列 int Convert.ToInt32 ( "0xFFFF" , 16 );

「Convert.ToInt32("0xFFFF", 16);」は文字列の先頭に「0x」が付いていても変換できます。

使用例は以下の通りです。

public static void Foo()
{
    // 文字列を2進数として intに変換
    int inum1 = Convert.ToInt32("110000111010", 2);
    // > inum1 = 3130

    // 文字列を8進数として intに変換
    int inum2 = Convert.ToInt32("6072", 8);
    // > inum2 = 3130

    // 文字列を10進数として intに変換
    int inum3 = Convert.ToInt32("3130");
    // > inum3 = 3130

    // 先頭に0xが付いた16進数文字列を intに変換
    int inum4 = Convert.ToInt32("0xC3A", 16);
    // > inum4 = 3130

    // int 以外にもほぼすべての基本型に変換できる。
    // ただし 16 が指定できるオーバーロードが無いものもある
    byte _b = Convert.ToByte(str, 16);
}

短いですが以上です。

【はてなブログ】投稿記事のURLを一括取得する

とある事情で自分のブログの投稿した記事の全URLのリストアップが必要になったのでリストアップするためのプログラムをC#で書いてみました。

せっかくなのでコードを公開しようと思います。

サイトマップの形式

まずはサイトマップのデータ形式を確認します。はてなブログのサイトマップの形式は2020年7月8日現在以下の通りです。

【サイト全体の概要】sitemap.xml

まずはトップの sitemap.xml です。このURLには各月ごとにまとめられた子サイトマップへのリンク集となっています。

以下のぼくのサイトの例では1か月単位で タグにURLが順番に並んでいます。

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://takap-tech.com/sitemap_common.xml</loc>
    <lastmod>2020-07-08T01:50:34+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://takap-tech.com/sitemap_periodical.xml?year=2020&amp;month=7</loc>
    <lastmod>2020-07-08T01:50:34+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://takap-tech.com/sitemap_periodical.xml?year=2020&amp;month=6</loc>
    <lastmod>2020-07-08T01:50:34+09:00</lastmod>
  </sitemap>
  <sitemap>
  ... 以下繰り返し

【月ごとの記事一覧】sitemap_periodical.xml

sitemap.xml にあったリンク先は各月ごとの投稿記事の一覧のURLです。

ここに具体的な各記事へのURLが記載されているのでここからURLを抽出します。

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://takap-tech.com/entry/2020/06/22/220549</loc>
    <lastmod>2020-06-22T22:13:01+09:00</lastmod>
  </url>
  <url>
    <loc>https://takap-tech.com/entry/2020/06/20/232208</loc>
    <lastmod>2020-06-24T22:26:57+09:00</lastmod>
  </url>
  <url>
  ... 以下繰り返し
</urlset>

各記事のURLをC#で取得する

プログラム言語はC#を使用します。

確認環境

この記事は以下の環境で動作確認しました。

  • C# 8.0
  • VisualStudio 2019
  • .NET Core 3.1

実装コード

C#で記事URLを取得するには HttpClient を使用するのが一番簡単だと思います。

コードは以下の通りです。

まず sitemap.xml の内容を HttpClient で取得し、内容を XDocument でパースします。各月ごとのリンクが取れるのでそれを読みに行って各記事のURLを取得しコンソールに出力します。

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Xml.Linq;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;

internal class AppMain
{
    private static readonly XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9";

    public static void Main(string[] args)
    {
        string domain = args[0]; // コマンドラインからドメイン名を指定
        string rootUrl = $"https://{domain}/sitemap.xml";

        using var client = new HttpClient();

        foreach (var sub in parseRoot(client, rootUrl))
        {
            foreach (var path in parseSub(client, sub))
            {
                Console.WriteLine(path);
                // string title = getTitle(url).Result;
                // Console.WriteLine($"{url}, {title}");
            }
        }
    }

    public static IEnumerable<string> parseRoot(HttpClient client, string url)
    {
        string body = client.GetStringAsync(url).Result;

        var xml = XDocument.Parse(body);
        var e1 = xml.Element(ns + "sitemapindex");
        var e2 = e1.Elements(ns + "sitemap");

        foreach (var e3 in e2)
        {
            yield return e3.Element(ns + "loc").Value;
        }
    }

    public static IEnumerable<string> parseSub(HttpClient client, string url)
    {
        string body = client.GetStringAsync(url).Result;

        var xml = XDocument.Parse(body);
        var e1 = xml.Element(ns + "urlset");
        var e2 = e1.Elements(ns + "url");

        foreach (var e3 in e2)
        {
            yield return e3.Element(ns + "loc").Value;
        }
    }

    // ★★★AngleSharp を利用した記事タイトルの取得
    // using AngleSharp.Html.Dom;
    // using AngleSharp.Html.Parser;
    public static async Task<string> getTitle(string url)
    {
        using var client = new HttpClient();
        using var stream = await client.GetStreamAsync(new Uri(url));
        var parser = new HtmlParser();
        IHtmlDocument doc = await parser.ParseDocumentAsync(stream);
        return doc.Title;
    }
}

実行結果

コマンドラインから以下のようにドメインを指定してプログラムを実行すると以下のように出力されます。

// ドメインを引数にアプリを実行する
> url-listup.exe takap-tech.com

// こんな感じにコンソールに出力される
> https://takap-tech.com/
> https://takap-tech.com/about
> https://takap-tech.com/entry/2020/07/08/015033
> https://takap-tech.com/entry/2020/06/22/220549
> https://takap-tech.com/entry/2020/06/20/232208
> https://takap-tech.com/entry/2020/06/18/002237
> https://takap-tech.com/entry/2020/06/11/003228
> https://takap-tech.com/entry/2020/06/05/005816
> https://takap-tech.com/entry/2020/06/05/001711
> https://takap-tech.com/entry/2020/04/30/152534
> https://takap-tech.com/entry/2020/04/28/211622

【C#】文字列や数値をenum型に変換する

ある任意の文字列や数値から特定のEnumに変換する方法です。

確認環境

  • C#8.0/7.3
  • VisualStudio2019
  • .NET Framwework 4.8/4.7.3
  • Windows10

バージョン依存性があるコードが含まれます。

文字列をEnumに変換する方法

まず enum が以下のように定義されているとします。

// 以下のようにenumが定義されているものとする
public enum EnumSample
{
    Ika = 0,
    Tako,
    Suzuki
}

シンボル名文字列から enum 型への変換

あるEnumのシンボル名からEnum形への変換方法jは以下の通りです。

変換は標準の機能として Enum クラスに Parse 系のメソッドが存在するためこれを利用します。

public static void Foo()
{
    // 変換方法(1) .NET Core の場合
    //
    // 補足:
    // この方法は.NET Core 限定。NET Framework にはメソッドが存在しない
    //
    // 変換できることが確実な場合このように記述する
    // 変換できない場合、「System.ArgumentException: 'Requested value 'sake' was not found.'」
    // 
    var e1 = Enum.Parse<EnumSample>("sake");

    // 変換方法(2) .NET Framework の場合
    //
    // 変換できることが確実な場合このように記述する
    // 変換できない場合、「System.ArgumentException: 'Requested value 'sazae' was not found.'」
    //
    var e2 = (EnumSample)Enum.Parse(typeof(EnumSample), "sazae");

    // 変換方法(2), .NET Core/Framework 共通
    //
    // 変換できるか分からないときにこちらを使用して
    // 変換できたか確認してから使用する。
    //
    if (Enum.TryParse("ika", out EnumSample result))
    {
        // 変換が成功したときに result に結果が入っている
    }
    else
    {
        // 失敗した場合こっち
    }
}

ターゲットプラットフォームが変更される場合、ジェネリック指定できたほうが記述が簡単になるので、上記変換(1), (2)はまとめて以下のように定義していた方が便利かもしれません。

// .NET Framework にジェネリック版の Parse メソッドが存在しないが .NET Core と同じように扱う対応
public static class MyEnum
{
#if NETFRAMEWORK
    public static TEnum Parse<TEnum>(string name) where TEnum : struct
    {
        return (TEnum)Enum.Parse(typeof(TEnum), name);
    }
#else
    public static TEnum Parse<TEnum>(string name) where TEnum : struct
    {
        return Enum.Parse<TEnum>(name);
    }
#endif
}

数字文字列から enum 型への変換

ある文字列変数に "1" などの数値文字列が入っている場合、enum 型に存在しない値でも変換できてしまうため Enum.IsDefined() メソッドで数字に対応する enum のシンボルが定義されているか確認する必要があります。

基本的に先述のシンボル名文字列からの変換と同じですが変換する前に IsDefined を使って存在するかどうかを確認してから変換を行います。

// 変換方法(1) .NET Core の場合
//
// 補足:
// この方法は.NET Core 限定。NET Framework にはメソッドが存在しない
//
// 変換できることが確実な場合このように記述する
// 変換できない場合、「System.ArgumentException: 'Requested value 'sake' was not found.'」
// 
string str1 = "10";
if (Enum.IsDefined(typeof(EnumSample), str1)) // 事前に定義が存在するかどうか確認する
{
    var e1 = Enum.Parse<EnumSample>(str1);
}

// 変換方法(2) .NET Framework の場合
//
// 変換できることが確実な場合このように記述する
// 変換できない場合、「System.ArgumentException: 'Requested value 'sazae' was not found.'」
//
string str2 = "Tako";
if (Enum.IsDefined(typeof(EnumSample), str2))
{
    var e2 = (EnumSample)Enum.Parse(typeof(EnumSample), str2);
}

// 変換方法(2), .NET Core/Framework 共通
//
// 変換できるか分からないときにこちらを使用して
// 変換できたか確認してから使用する。
//
string str3 = "Ika";
if (Enum.IsDefined(typeof(EnumSample), str3) && 
    Enum.TryParse(str3, out EnumSample result))
{
    // 変換が成功したときに result に結果が入っている
}
else
{
    // 失敗した場合こっち
}

数値から enum 型への変換

次は数値から enum 型への変換です。

変換自体は数値を enum 型にキャストするだけで変換可能ですが、こちらも存在しないシンボルの値にも変換できてしまうため事前に IsDefined で確認が必要になります。

// 変換方法(1)、普通にキャストする
int num1 = 0;
if (Enum.IsDefined(typeof(EnumSample), num1)) // 事前に定義が存在するかどうか確認する
{
    var e1 = (EnumSample)num1; // キャストで変換できる
}

// 変換方法(2) TryPaeseを使う
int num2 = 10;
if (Enum.IsDefined(typeof(EnumSample), num2) && 
    Enum.TryParse(num2.ToString(), out EnumSample result))
{
    // 変換が成功したときに result に結果が入っている
}
else
{
    // 失敗した場合こっち
}

相互変換をサポートするクラス

3種類のケースを見てきましたが結局文字列に何が入っているかで書き分ける必要があり、数値の場合とコードが違うなどで面倒なため、全て1つのメソッド解決できるユーティリティを紹介したいと思います。

使い方

先に使い方を紹介します。EnumUtil クラスに Parse, TryParse を実装しています。

//
// (1) Parse メソッドの使い方
//

var e0 = EnumUtil.Parse<EnumSample>("10");
// > 値が存在しないので ArgumentException

var e1 = EnumUtil.Parse<EnumSample>("Tako");
// > e1 = Tako

var e2 = EnumUtil.Parse<EnumSample>(2);
// > e1 = Suzuki

var e3 = EnumUtil.Parse<EnumSample>("2");
// > これはエラーになるので注意!

//
// (2) TryParse メソッドの使い方
//

if (EnumUtil.TryParse("10", out EnumSample result1))
{
    // 変換できたとき
}
else
{
    // 変換できなかったとき
}

// 大文字・小文字を区別したくない時は igoreCase = true で使用する
if (EnumUtil.TryParse("tako", true, out EnumSample result2))
{
    // 変換できたとき
}
else
{
    // 変換できなかったとき
}

EnumUtilクラス

実際の実装です。

.NET Framework でも使用可能なようにジェネリック版の Parse は使用していません。

// 任意の値を enum 型に変換するための機能を定義します
public static class EnumUtil
{
    public static TEnum Parse<TEnum>(object value, bool ignoreCase = false) where TEnum : struct
    {
        if (Enum.IsDefined(typeof(TEnum), value))
        {
            return (TEnum)Enum.Parse(typeof(TEnum), value.ToString(), ignoreCase);
        }
        else
        {
            throw new ArgumentException($"'{value}' is not found.");
        }
    }

    public static bool TryParse<TEnum>(object value, out TEnum result) where TEnum : struct
    {
        result = default;
        return Enum.IsDefined(typeof(TEnum), value) && 
            Enum.TryParse(value.ToString(), out result);
    }

    public static bool TryParse<TEnum>(object value, 
        bool ignoreCase, out TEnum result) where TEnum : struct
    {
        result = default;
        return Enum.IsDefined(typeof(TEnum), value) && 
            Enum.TryParse(value.ToString(), ignoreCase, out result);
    }
}

文字列変数を直接 enum に変換する

ここからは余談で、string 型に拡張メソッドを定義して直接 enum に変換してみます。

使い方

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

string name1 = "Tako";

// (1) 文字列の変数から直接 Parse して enum を得る
var e1 = name.Parse<EnumSample>();

string name2 = "Same";

// (2) 文字列の変数から直接 TryParse して変換できるか確認後に enum を得る
if(name.TryParse(out EnumSample ret))
{
    // 変換に成功した場合
}
else
{
    // 失敗した場合
}

実装コード:StringExtensionクラス

実装はすごく簡単で先ほどの EnumUtil を間接的に使用します。

public static class StringExtension
{
    // 文字列から TEnum で指定した enum に変換する
    public static TEnum Parse<TEnum>(this string self) where TEnum : struct
    {
        return EnumUtil.Parse<TEnum>(self);
    }

    public static bool TryParse<TEnum>(this string self, out TEnum result) where TEnum : struct
    {
        return EnumUtil.TryParse(self, out result);
    }

    public static bool TryParse<TEnum>(this string self, 
        object value, bool ignoreCase, out TEnum result) where TEnum : struct
    {
        return EnumUtil.TryParse(self, ignoreCase, out result);
    }
}

少し長くなってしまいましたが以上です。

関連リンク

takap-tech.com

【C#】基本型に範囲チェック機能を追加する

はじめに

ある変数が範囲内に収まっていれば新しい値を代入する処理などの「範囲を意識した処理」というものはプログラミングをしているを割と良く出てくる課題です。コードを書くと以下のように記述できます。

// value が 0~10の範囲内なら新しい値を代入する
double value = 10.5;
if (value <= 10 && value >= 0)
{
    value = 21.5;
}

こういった判定の繰り返しの記述で毎回コードを書かないように処理を汎用化したいと思います。この処理を各型の「拡張メソッド」として定義して上記判定文を以下のように簡単化します。

// value が 0~10の範囲内なら新しい値を代入する
value.Assign(21.5, 0, 10);

目次

今回の記事の目次は以下の通りです。

確認環境

  • VisualStudio 2019
  • .NET Core 3.1 (C#8.0)
  • Windows 10

実装した処理一覧

今回実装したメソッドの概要です。

# メソッド名 説明
1 Assign(v, min, max) 変数値が min ~ max の範囲内なら v を代入して true / 範囲外の場合代入せずに false を返す
2 AssignIfOrMore(v, min) 変数値が min 以上なら v を代入して true / 範囲外の場合代入せずに false を返す
3 AssignIfOrLess(v, max) 変数値が min 以下なら v を代入して true / 範囲外の場合代入せずに false を返す
4 IsInRange(min, max) 変数値が min ~ max の範囲内なら true / 範囲外なら false を返す
5 IsInRangeOrMore(min) 変数値が min 以上 true / 範囲外の場合代入せずに false を返す
6 IsInRangeOrLess(max) 変数値が min 以下なら true / 範囲外の場合代入せずに false を返す
7 Clamp(min, max) 変数値が範囲内に収まるように min以下ならmin, max 以上なら max の値を返す
8 ClampSelf(min, max) 変数値が範囲内に収まるように min以下ならmin, max 以上なら max の値を自分自身に設定する

上記メソッドのサポートしている型は以下の通りです。(.NETの基本型は全部サポートしました…大変だった、、、

# 型名 サポート状況
1 byte サポート済み
2 sbyte サポート済み
3 decimal サポート済み
4 double サポート済み
5 float サポート済み
6 int サポート済み
7 uint サポート済み
8 long サポート済み
9 ulong サポート済み
10 short サポート済み
11 ushort サポート済み

使い方

前述のメソッドの使い方は以下の通りです。

Assign(v, min, max):現在値が範囲内なら新しい値を代入する

// (1) 
int value_1 = 10;
bool ret_1 = value_1.Assign(30, 0, 10);
// > ret_1=true, value_1=30, 0~10の範囲外だったので新しい値を代入

int value_2 = 10;
bool ret_2 = value_2.Assign(30, 0, 5);
// > ret_2=flase, value_2=10, 0~5の範囲外なので変化しない

AssignIfOrMore(v, min):現在値が min 以上なら値を代入する

int value_1 = 10;
bool ret_1 = value_1.AssignIfOrMore(30, 0);
// > ret_1=true, value_1=30, 現在値が0以上だったので新しい値を代入

int value_2 = 10;
bool ret_2 = value_2.AssignIfOrMore(30, 20);
// > ret_2=flase, value_2=10, 現在値が20以上ではないので代入しない

AssignIfOrLess(v, max):現在値が min 以下なら値を代入する

int value_1 = 10;
bool ret_1 = value_1.AssignIfOrLess(30, 100);
// > ret_1=true, value_1=100, 現在値が100以下だったので新しい値を代入

int value_2 = 10;
bool ret_2 = value_2.AssignIfOrLess(30, 5);
// > ret_2=flase, value_2=10, 現在値が5以下ではないので代入しない

IsInRange(min, max):変数値が min ~ max の範囲内か確認する

int value_1 = 50;
bool ret_1 = value_1.IsInRange(0, 100);
// > ret_1=true, 変数値が0~100の範囲内なのでtrue

int value_2 = 200;
bool ret_2 = value_2.IsInRange(0, 100);
// > ret_2=flase, 変数値が0~100の範囲外なのでfalse

IsInRangeOrMore(min): 変数値が min 以上か確認する

int value_1 = 100;
bool ret_1 = value_1.IsInRangeOrMore(50);
// > ret_1=true, 変数値が50以上なのでtrue

int value_2 = 100;
bool ret_2 = value_2.IsInRangeOrMore(200);
// > ret_2=flase, 変数値が200以上ではないのでfalse

IsInRangeOrLess(max):変数値が min 以下か確認する

int value_1 = 50;
bool ret_1 = value_1.IsInRangeOrLess(100);
// > ret_1=true, 変数値が100以下なのでtrue

int value_2 = 50;
bool ret_2 = value_2.IsInRangeOrLess(30);
// > ret_2=flase, 変数値が30以下ではないのでfalse

Clamp(min, max):範囲内に収まる値を返す

int value_1 = 100;
int ret_1 = value_1.Clamp(20, 30);
// > ret_1=30, 30以上なので範囲内の最大値30を返す

int value_2 = 10;
int ret_2 = value_2.Clamp(20, 30);
// > ret_2=20, 20以下なので範囲内の最小値20を返す

ClampSelf(min, max):変数値を範囲内に収める

int value_1 = 100;
value_1.ClampSelf(20, 30);
// > value_1=30, 30以上なので範囲内の最大値30を設定する

int value_2 = 10;
value_2.ClampSelf(20, 30);
// > ret_2=20, 20以下なので範囲内の最小値20を設定する

実装コード

使い方の項目のメソッドを各基本型に拡張メソッドとして実装します。

RangeExtensionクラス

全部のコードがここに入っています。死ぬほど長いので全部コピペしてエディタ上で見たほうがいいかもしれません。

/// <summary>
/// 基本型の機能を拡張し、範囲に関係する機能を追加します。
/// </summary>
public static class RangeExtension
{
    // (1) 現在値が範囲内なら代入してtrue、範囲外の場合代入せずにfalse
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref byte self, byte newValue, byte min, byte max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref sbyte self, sbyte newValue, sbyte min, sbyte max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref decimal self, decimal newValue, decimal min, decimal max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref double self, double newValue, double min, double max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref float self, float newValue, float min, float max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref int self, int newValue, int min, int max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref uint self, uint newValue, uint min, uint max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref long self, long newValue, long min, long max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref ulong self, ulong newValue, ulong min, ulong max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref short self, short newValue, short min, short max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref ushort self, ushort newValue, ushort min, ushort max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }

    // (2) 現在値が最小値以上なら代入してtrue、下回っている場合代入せずにfalse
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref byte self, byte newValue, byte min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref sbyte self, sbyte newValue, sbyte min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref decimal self, decimal newValue, decimal min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref double self, double newValue, double min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref float self, float newValue, float min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref int self, int newValue, int min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref uint self, uint newValue, uint min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref long self, long newValue, long min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref ulong self, ulong newValue, ulong min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref short self, short newValue, short min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref ushort self, ushort newValue, ushort min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }

    // (3) 現在値が最大値以下なら代入してtrue、上回っている場合代入せずにfalse
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref byte self, byte newValue, byte max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref sbyte self, sbyte newValue, sbyte max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref decimal self, decimal newValue, decimal max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref double self, double newValue, double max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref float self, float newValue, float max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref int self, int newValue, int max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref uint self, uint newValue, uint max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref long self, long newValue, long max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref ulong self, ulong newValue, ulong max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref short self, short newValue, short max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref ushort self, ushort newValue, ushort max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }

    // (3) 現在値が範囲内に収まっているか確認する、true : 範囲内 / false : 範囲外
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsInRange(this byte self, byte min, byte max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this sbyte self, sbyte min, sbyte max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this decimal self, decimal min, decimal max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this double self, double min, double max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this float self, float min, float max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this int self, int min, int max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this uint self, uint min, uint max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this long self, long min, long max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this ulong self, ulong min, ulong max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this short self, short min, short max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this ushort self, ushort min, ushort max) => max <= self && self >= min;

    // (4) 現在値が指定した最小値以上かどうか確認する、true : 最小値以上 / false : 最小値未満
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this byte self, byte min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this sbyte self, sbyte min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this decimal self, decimal min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this double self, double min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this float self, float min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this int self, int min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this uint self, uint min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this long self, long min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this ulong self, ulong min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this short self, short min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this ushort self, ushort min) => self >= min;

    // (5) 現在値が指定した最大値以下かどうか確認する、true : 最大値以下 / false : 最大値より大きい
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this byte self, byte max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this sbyte self, sbyte max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this decimal self, decimal max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this double self, double max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this float self, float max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this int self, int max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this uint self, uint max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this long self, long max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this ulong self, ulong max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this short self, short max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this ushort self, ushort max) => max <= self;

    // (6) 自分自身から範囲内に収まるように値を取得します。
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static byte Clamp(this byte self, byte min, byte max)
    {
        byte value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static sbyte Clamp(this sbyte self, sbyte min, sbyte max)
    {
        sbyte value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static decimal Clamp(this decimal self, decimal min, decimal max)
    {
        decimal value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static double Clamp(this double self, double min, double max)
    {
        double value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static float Clamp(this float self, float min, float max)
    {
        float value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int Clamp(this int self, int min, int max)
    {
        int value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static uint Clamp(this uint self, uint min, uint max)
    {
        uint value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static long Clamp(this long self, long min, long max)
    {
        long value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static ulong Clamp(this ulong self, ulong min, ulong max)
    {
        ulong value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static short Clamp(this short self, short min, short max)
    {
        short value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static ushort Clamp(this ushort self, ushort min, ushort max)
    {
        ushort value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }

    // (7) 自分自身をが範囲外なら範囲内に収まるように調整します。
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref byte self, byte min, byte max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref sbyte self, sbyte min, sbyte max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref decimal self, decimal min, decimal max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref double self, double min, double max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref float self, float min, float max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref int self, int min, int max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref uint self, uint min, uint max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref long self, long min, long max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref ulong self, ulong min, ulong max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref short self, short min, short max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref ushort self, ushort min, ushort max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
}

【C#】Anyメソッド解説 & 範囲指定できるように拡張する

LinqのAnyメソッド使い方と範囲指定できるように機能拡張を行います。

Anyメソッドとは

簡単な説明

まず初めにAnyメソッドの説明です。

AnyメソッドはIEnumerableの拡張メソッド(=Linq)として定義されていて指定した配列やリストに中身があるかどうかを判定できます。

// Anyの宣言(1):中身があるかどうか判定できる
public static bool Any<TSource>(this IEnumerable<TSource> source);
// Anyの宣言(2):predicateで指定した条件が中身に存在するか確認できる
public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

Anyメソッドの使い方

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

// Anyの宣言(1)の使い方
public static bool Any<TSource>(this IEnumerable<TSource> source);

public void Foo()
{
    // リストに中身があるかどうか判定する
    
    var list_1 = new List<int>();
    bool result_1 list_1.Any();
    // > result_1 = false : 空のリストの場合 false
    
    var list_2 = new List<int>() { 0, 1, 2, 3, 4, 5, 6 };
    bool result_2 = list_2.Any();
    // > result_2 = true : 中身があれば true
}

// Anyの宣言(2):の使い方
public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

public void Foo()
{
    // リストに条件に一致する中身があるかどうか判定する
    
    var list_1 = new List<int>();
    bool result_1 list_1.Any(p => p > 5 /*5以上の要素があるか?*/);
    // > result_1 = false : 条件を指定しても空なので false
    
    var list_2 = new List<int>() { 0, 1, 2, 3, 4, 5, 6 };
    bool result_2 = list_2.Any(p => p > 5 /*5以上の要素があるか?*/);
    // > result_2 = true : 5, 6が条件に一致するため true, p > 10 とすれば false
}

サンプルではListに対してAnyメソッドを実行していますが配列に対しても同様の操作が可能です。

更に、IEnumerableを継承しているほかの型でもAnyで確認ができます。

Anyメソッドは使用頻度(低)

余談ですがAnyメソッドは単体では使う機会はほぼ無いです。要素があるかどうかは「Count」や「Length」で確認できます。配列とListで操作を統一できるメリットはありますが、内部でイテレータを使用しているため速度がCountやLengthよりかなり遅いです。

それに条件を指定するほうも、同じLinqでContainsと機能がほぼ被っているのでメソッド名から意図がくみ取りやすいContainsを使用します。最近見かけませんが、昔に Linq to Object or SQL という構文中で割と使っていましたが最近ほぼ使わないですね。

Anyに範囲を指定するように拡張する

タイトルの件ですが、そんなAnyメソッドですが利用性を向上するために範囲指定できるようにしたいと思います。

EnumerableExtensionクラス

以下の通りEnumerableExtensionクラスを作成してIEnumerableの拡張メソッドとして処理を作成したいと思います。

// IEnumerableの拡張メソッドを定義するクラス
public static class EnumerableExtension
{
    // (1) 要素の開始位置を指定して要素の有無を確認します。
    public static bool Any<TSource>(this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate, int startIndex)
    {
        int len = source.Count();
        if (startIndex < 0 || startIndex > len)
            throw new ArgumentOutOfRangeException(nameof(startIndex), $"inde={startIndex}");
        
        int i = 0;
        foreach (var item in source)
        {
            if (i++ < startIndex) continue;
            if (predicate(item)) return true;
        }
        return false;
    }

    // (2) 要素の対象範囲を指定して要素の有無を確認します。
    public static bool Any<TSource>(this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate, int startIndex, int endIndex)
    {
        int len = source.Count();
        if (startIndex < 0 || startIndex > len) 
            throw new ArgumentOutOfRangeException(nameof(startIndex), $"nameof(startIndex)={startIndex}");
        if (endIndex < 0 || endIndex > len)
            throw new ArgumentOutOfRangeException(nameof(endIndex), $"nameof(endIndex)={endIndex}");

        int i = 0;
        foreach (var item in source)
        {
            if (i > endIndex) return false;
            if (i++ < startIndex) continue;
            if (predicate(item)) return true;
        }
        return false;
    }
}

指定できる範囲の指定は配列の要素番号で行います。

使い方

上記メソッドの使い方はそれぞれ以下の通りです。

拡張メソッドとして作成したためAnyと同じようにListに対してメソッド呼び出しで処理を記述できます。

public void Foo(params string[] args)
{
    // (1) 要素の開始位置を指定して要素の有無を確認する
    var list = new List<int>() { 0, 1, 2, 3, 4, 5, 6 };
    bool result_1 = list.Any(p => p > 5, 3); // リストの3つ目以降に5以上の値があるか?
    Console.WriteLine(result_1);
    // result_1 = true : p > 10 等にすると存在しないのでfalse
    
    // (2) 要素の対象範囲を指定して要素の有無を確認する
    bool result_2 = list.Any(p => p == 3, 2, 3); // リストの2つ目 ~ 4つ目の範囲内に3が存在するか?
    Console.WriteLine(result_2);
    // result_2 = true : p == 10 等にすると存在しないのでfalse
}

【参考】Anyメソッドの実装

.NET(Framework)のソースコードはReferencesourceというサイトで中身が確認できます。

今回紹介したAnyメソッドの実装は以下の通りです。

https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,8788153112b7ffd0

public static bool Any<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    using (IEnumerator<TSource> e = source.GetEnumerator()) {
        if (e.MoveNext()) return true;
    }
    return false;
}

public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
    if (source == null) throw Error.ArgumentNull("source");
    if (predicate == null) throw Error.ArgumentNull("predicate");
    foreach (TSource element in source) {
        if (predicate(element)) return true;
    }
    return false;
}

見ればわかると思いますが、Any()はイテレータで最初の要素が取れればtrueで要素があると判定しています。

あんまり効率が良くなさそうです。

Any(predicate)は最初から最後までforeachでループして要素があるか確認してます。IEnumerableは長さが無限の場合が稀にあるので安易に実行すると処理が戻ってこない可能性があります。まぁ滅多に無いので頭の片隅に置いておくくらいで大丈夫です。

【C#】配列に要素を追加・削除する、中身をシャッフルする

C#の配列に要素を追加したり削除したり中身をランダム化する方法の紹介です。

配列は一度宣言してしまうとサイズ変更は(基本的に)できないです。そういった事がした場合は動的リストの「System.Collections.Generic」名前空間にある「List」を使用しますが、パフォーマンスや制約などで配列でデータを持っているとろへ要素を追加したりしたくなったのでコードを書いてみました。

先に注意点ですが、(基本的に)できないことを行っているのでパフォーマンスが同様の操作をListに対して行うよりだいぶ悪いと思います。もし要素の追加・削除を頻繁に行うようであればListの仕様を検討ください。

実装コード

かなり長いので、このコードはコピペして使い方の項目まで飛ばしても問題ありません。

ArrayExtensionクラス

各操作を配列に対する拡張メソッドの形式で定義しています。

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

/// <summary>
/// 配列に対する拡張機能を提供します。
/// </summary>
public static class ArrayExtension
{
    //
    // (1) 配列の要素に対する基本操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Exists{T}(T[], Predicate{T})"/> を標準の検索方法で拡張メソッド化します。
    /// </summary>
    public static bool Exists<T>(this T[] array, T item) => Array.Exists(array, p => p.Equals(item));

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Exists{T}(T[], Predicate{T})"/> を拡張メソッド化します。
    /// </summary>
    public static bool Exists<T>(this T[] array, Predicate<T> match) => Array.Exists(array, match);

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Find{T}(T[], Predicate{T})"/> を標準の検索方法で拡張メソッド化します。
    /// </summary>
    public static T Find<T>(this T[] array, T item) => Array.Find(array, p => p.Equals(item));

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Find{T}(T[], Predicate{T})"/> を拡張メソッド化します。
    /// </summary>
    public static T Find<T>(this T[] array, Predicate<T> match) => Array.Find(array, match);

    /// <summary>
    /// 配列に対する操作 <see cref="Array.FindIndex{T}(T[], Predicate{T})"/> を拡張メソッド化します。
    /// (見つからない場合-1)
    /// </summary>
    public static int FindIndex<T>(this T[] array, T item) => Array.FindIndex(array, p => p.Equals(item));

    /// <summary>
    /// 配列に対する操作 <see cref="Array.FindIndex{T}(T[], Predicate{T})"/> を準の検索方法で拡張メソッド化します。
    /// (見つからない場合-1)
    /// </summary>
    public static int FindIndex<T>(this T[] array, Predicate<T> match) => Array.FindIndex(array, match);

    //
    // (2) 配列に要素を追加する
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 配列の先頭に値を追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] InsertTop<T>(this T[] array, T value)
    {
        var newArray = new T[array.Length + 1];
        newArray[0] = value;
        Array.Copy(array, 0, newArray, 1, array.Length);
        return newArray;
    }

    /// <summary>
    /// 配列の最後に値を追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] InsertLast<T>(this T[] array, T value)
    {
        var newArray = new T[array.Length + 1];
        Array.Copy(array, 0, newArray, 0, array.Length);
        newArray[newArray.Length - 1] = value;
        return newArray;
    }

    /// <summary>
    /// 指定した位置に値を追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] Insert<T>(this T[] array, int index, T value)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException($"index is out of range. index={index}.");

        var newArray = new T[array.Length + 1];
        Array.Copy(array, 0, newArray, 0, index); // インデックスより前
        newArray[index] = value;
        Array.Copy(array, index, newArray, index + 1, array.Length - index);

        return newArray;
    }

    /// <summary>
    /// 指定した位置に collection で指定した要素を連続で追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] InsertRange<T>(this T[] array, int index, IEnumerable<T> collection)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException($"index is out of range. index={index}.");

        int len = collection.Count();
        var newArray = new T[array.Length + len];

        Array.Copy(array, 0, newArray, 0, index); // インデックスより前

        int i = 0;
        foreach (var item in collection)
        {
            newArray[index + i++] = item;
        }
        Array.Copy(array, index, newArray, index + len, array.Length - index);

        return newArray;
    }

    //
    // (3) 特定の要素を削除
    // - - - - - - - - - - - - - - - - - - - -

    // 補足:
    // 以下の削除処理は、メモリ効率と動作速度がかなり悪いため
    // 頻繁にこのような操作が発生する場合は System.Collections.Generic.List<T> の使用を検討すること。

    /// <summary>
    /// 配列から指定した位置の要素を削除した新しい配列を取得します。
    /// </summary>
    public static T[] RemoveAt<T>(this T[] array, int index)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException($"index is out of range. index={index}.");

        var newArray = new T[array.Length - 1];
        Array.Copy(array, 0, newArray, 0, index); // インデックスより前
        Array.Copy(array, index + 1, newArray, index, array.Length - index - 1); // インデックスより後

        return newArray;
    }

    /// <summary>
    /// 配列のいちばん最初に見つかった1つの要素を削除し新しい配列を取得します。削除されなかった場合null を返します。
    /// </summary>
    public static T[] RemoveFirst<T>(this T[] array, T item)
    {
        int index = array.FindIndex(item);
        if (index == -1) return null;

        return array.RemoveAt(index);
    }

    /// <summary>
    /// 指定した条件を満たす配列のいちばん最初に見つかった要素を削除し新しい配列を取得します。
    //// 削除されなかった場合null を返します。
    /// </summary>
    public static T[] RemoveFirst<T>(this T[] array, Predicate<T> match)
    {
        int index = array.FindIndex(match);
        if (index == -1) return null;

        return array.RemoveAt(index);
    }

    /// <summary>
    /// 指定した要素と同じ値を配列から全て削除します。削除されなかった場合 null を返します。
    /// </summary>
    public static T[] RemoveAll<T>(this T[] array, T item) => array.RemoveAll(elem => elem.Equals(item));

    /// <summary>
    /// 指定した条件を満たす要素を配列から全て削除します。削除されなかった場合 null を返します。
    /// </summary>
    public static T[] RemoveAll<T>(this T[] array, Predicate<T> match)
    {
        var list = new List<T>();
        for (int i = 0; i < array.Length; i++)
        {
            if (!match(array[i]))
            {
                list.Add(array[i]);
            }
        }
        return list.Count == array.Length ? null : list.ToArray();
    }

    //
    // (4) 配列に対するランダムな操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 配列からランダムに要素を1つ取り出します。
    /// </summary>
    public static T PickupOne<T>(this T[] array)
    {
        return array[Rand.Next(0, array.Length)];
    }

    /// <summary>
    /// 配列からランダムに1つ要素を取り出した後その要素を配列から削除します。
    /// </summary>
    public static (T[] /*newArray*/, T /*poppedItem*/) PickupOneAndRemove<T>(this T[] array)
    {
        var newArray = new T[array.Length - 1];
        int index = Rand.Next(0, array.Length);
        T item = array[index];
        for (int i = 0, j = 0; i < array.Length; i++)
        {
            if (i == index)
            {
                continue;
            }
            newArray[j++] = array[i];
        }
        return (newArray, item);
    }

    /// <summary>
    /// 指定した配列をランダムに並び替えます。
    /// </summary>
    public static void Shuffle<T>(this T[] array)
    {
        for (int i = 0; i < array.Length; i++)
        {
            array.Swap(i, Rand.Range(0, array.Length));
        }
    }

    // 元の配列はそのままで新しいランダムな配列を作る取得します。

    /// <summary>
    /// 指定した配列からランダム化された新しい配列を作成・取得します。
    /// </summary>
    public static T[] GetNewRandomArray<T>(this T[] array)
    {
        var newArray = new T[array.Length];
        Array.Copy(array, newArray, array.Length);
        newArray.Shuffle();
        return newArray;
    }

    /// <summary>
    /// 配列の指定した2つのインデックス間の値を入れ替えます。
    /// </summary>
    public static void Swap<T>(this T[] array, int i, int j)
    {
        T tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    //
    // (5) Linq風の便利な操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 指定した配列をリストに変換します。
    /// </summary>
    public static List<T> ToList<T>(this T[] array)
    {
        var list = new List<T>();
        for (int i = 0; i < array.Length; i++)
        {
            list.Add(array[i]);
        }
        return list;
    }

    /// <summary>
    /// 指定した配列を述語に従って新しい型の配列に変換します。
    /// </summary>
    public static Dest[] Convert<T, Dest>(this T[] array, Func<T, Dest> func)
    {
        var newArray = new Dest[array.Length];
        for (int i = 0; i < array.Length; i++)
        {
            newArray[i] = func(array[i]);
        }
        return newArray;
    }

    /// <summary>
    /// 配列に対する操作 <see cref="Array.ForEach{T}(T[], Action{T}))"/> を拡張メソッド化します。
    /// </summary>
    public static void ForEach<T>(this T[] array, Action<T> action)
    {
        Array.ForEach(array, action);
    }

    //
    // (6) 配列に対するソート操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// <see cref="Array.Sort(Array)"/> を拡張メソッド化します。
    /// </summary>
    public static void Sort<T>(this T[] array) => Array.Sort(array);

    /// <summary>
    /// <see cref="Array.Sort{T}(T[], Comparison{T})"/> を拡張メソッド化します。
    /// </summary>
    public static void Sort<T>(this T[] array, Comparison<T> comparer) => Array.Sort(array, comparer);
}

使い方

以下、上記クラスの使い方になります。

配列にに要素が含まれているかどうかを確認・検索する(Exist, Find系)

要素の追加・削除などの前に配列に要素が存在するかどうかを確認するExist、要素がどの位置に存在するのかを確認するFindメソッドの使い方になります。

public static void Main(params string[] args)
{
    //
    // (1) 配列の要素に対する基本操作
    // - - - - - - - - - - - - - - - - - - - -
    int[] array = new int[] { 1, 2, 3, 4, 5, 6 };

    // (1-1) 要素が存在するか確認する
    string msg = array.Exists(3) ? "要素は存在します。" : "要素は存在しません。";
    // > msg = 要素は存在します。

    // (1-2) 条件を指定の要素が存在するか確認する
    string msg2 = array.Exists(elem => elem >= 3) ? "要素は存在します。" : "要素は存在しません。";
    // > msg2 = 要素は存在します。

    // (2-1) 指定した値を取得
    var value = array.Find(5);
    // > value = 5

    // (2-2) 条件を指定して要素を取得
    int value2 = array.Find(elem => elem > 5); // 5より大きい最初の値を取得(例としてちょっと微妙
    // > value2 = 6

    // (3-1) 指定した位置の要素を取得
    int index = array.FindIndex(3);
    Console.WriteLine($"3 は index={index} の位置にに存在します。");

    // (3-2) 指定した条件に一致する要素を取得
    int index2 = array.FindIndex(elem => elem >= 3);
    Console.WriteLine($"elem > 3 を満たす要素は index={index2} の位置にに存在します。");
}

配列に値を追加・挿入する(Insert系)

配列に対して値を追加・挿入する操作の説明です。

注意点として、Insertすると配列の要素数が増えますが、元の配列はそのままにして戻り値に要素が追加された配列が帰ってくるので追加したら以降はその配列を使用します。

public static void Main(params string[] args)
{
    //
    // (2) 配列に要素を追加する
    // - - - - - - - - - - - - - - - - - - - -
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (1) 先頭に要素を追加する
    int[] array1 = array.InsertTop(99);
    // > array1 = 99, 1, 2, 3, 4, 5

    // (2) 末尾に要素を追加する
    int[] array2 = array.InsertLast(99);
    // > array2 = 1, 2, 3, 4, 5, 99

    // (3) 指定した位置に要素を追加する
    int[] array3 = array.Insert(2, 99);
    // > array3 = 1, 2, 99, 3, 4, 5

    // (4) 指定した位置に複数の要素を追加する
    int[] array4 = array.InsertRange(3, new int[] { 99, 98, 97, 96 });
    // > array4 = 1, 2, 3, 99, 98, 97, 96, 4, 5
}

配列から値を削除する(Remove系)

配列から特定の要素を削除する処理の説明です。

これもInsert系と同じく元の配列はそのままで、戻り値に値が削除された配列が戻るので以降これを使用します。

public static void Main(params string[] args)
{
    //
    // (3) 特定の要素を削除
    // - - - - - - - - - - - - - - - - - - - -
    int[] array = new int[] { 1, 2, 3, 3, 4, 4, 5 };

    // (1) 指定した位置の要素を削除
    int[] array1 = array.RemoveAt(3);
    // array1 = 1, 2, 3, 4, 4, 5 (3がひとつ削除された新しい配列が戻る)

    // (2-1) 最初に見つかった要素を削除
    int[] array2 = array.RemoveFirst(2);
    // array2 = 1, 3, 3, 4, 4, 5 (2が削除された新しい配列が戻る)

    // (2-2) 指定した条件を満たす最初に見つかった要素を削除
    int[] array3 = array.RemoveFirst(elem => elem > 3); // 3より大きい最初の要素を削除
    // array3 = 1, 2, 3, 3, 4, 5 (4がひとつ削除された新しい配列が戻る)

    // (3-1) 指定した値に一致する要素をすべて削除
    int[] array4 = array.RemoveAll(3);
    // array4 = 1, 2, 4, 4, 5 (3が全部削除された新しい配列が戻る)

    // (3-2) 指定した条件に一致するすべての要素を削除
    int[] array5 = array.RemoveAll(elem => elem > 3); // 3より大きい要素を全部削除
    // array5 = 1, 2, 3, 3
}

配列に対しランダムな操作を行う

以下は、ゲームでは割とよく使う配列に対するランダムな操作の説明です。

配列からランダムに1つ値を取り出す(PickupOneメソッド)

配列の中からランダムに値を1つ選択して取り出します。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // ランダムに1つ取り出す
    int item = array.PickupOne();
    // > 2
}
配列からランダムに1つ値を取り出して要素を削除する(PickupOneAndRemoveメソッド)

上記のPickupメソッドに似ていますが、取り出した後に値を配列から値を削除します。

削除後の配列と取り出したデータは戻り値のタプルで受け取れます。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // ランダムに1つ取り出して要素を削除
    var (newArray, poppedItem) = array.PickupOneAndRemove();
    // > newArray  = 1, 3, 4, 5
    // > popedItem = 2
}
配列の内容をシャッフルする(Shuffleメソッド)

指定した配列の中身を全てランダムにシャッフルします。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // 配列をランダムに並び替える
    array.Shuffle();
    // > array = 3, 5, 1, 4, 2
}
シャッフルした新しい配列を取得する(GetNewRandomArrayメソッド)

上記のShuffleメソッドは元の配列に対してシャッフルしましたが、こちらは元の配列はそのままにしてシャッフル済みの新しい配列を戻り値で受け取れます。

public static void Main(params string[] args)
{
    // ランダムな新しい配列を取得する(元の配列はそのまま)
    var newArray2 = array.GetNewRandomArray();
    // > newArray2 = 2, 1, 5, 4, 3
}

Linq風の便利な操作

以下は、System.Linq風の処理を配列にも適用できるようにした処理です。

配列をリスト(List)に変換する(ToListメソッド)

配列をリスト(List)に変換します。List.ToArray()はLinqにあるのに逆が無いみたいなので実装しています。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (1) 配列をリストに変換する
    List<int> list = array.ToList(); // 同じ型のリストに変換できる
}
配列を別の型の配列に変換する(Convertメソッド)

配列を別の型の配列に変換します。各要素に対してラムダ式で変換方法を指定することによって別の型に変換することができます。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (2) 配列を他の型の配列に変換する
    float[] array2 = array.Convert(elem => (float)elem); // ラムダで変換条件を指定する
}
配列をforeachする(ForEachメソッド)

配列は通常のforeach文で使用できますが、このForEachメソッドを使うと処理内容をラムダで指定してLinq風に回す事ができます。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (2) 配列を他の型の配列に変換する
    float[] array2 = array.Convert(elem => (float)elem); // ラムダで変換条件を指定する
}

配列に対するソート処理(Sort系)

以下、配列をソートする方法です。指定方法は性的メソッドの「Array.Sort」の方が多彩ですが、このコードを入れておくと配列に対するメソッドとして処理を行うことができるようになります。多分この方が直観的なのかと思います。機能不足の場合は各自実装を追加したほうがいいかもしれません。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };
    array.Shuffle(); // (4)で紹介した内容をランダム化で内容をランダム化しておく
    // > 3, 1, 4, 5, 2

    // (1) 配列をソートする
    array.Sort();
    // > 1, 2, 3, 4, 5

    // (2) 整列条件を指定して配列をソートする
    array.Sort((a, b) => b - a); // この場合逆順の指定
    // > 5, 3, 4, 2, 1
}

リンク

ランダム化で使用しているRandクラスは以下リンクに掲載しているのでそちらも併せて確認ください。

takachan.hatenablog.com

すごく長くなってしましたが以上です。

Unityと.NET環境で乱数生成を共通化する

タイトルの通り、Unity環境下での乱数生成と非Unityの乱数生成を共通化しようという話です。

何故このようなことをするかというと、非UnityのMonoBehaviorも何もない環境で作成したアルゴリズムをUnityへインポートしたりするときに、UnityのRandomクラスと、.NET 標準のRandomクラスの性能が異なるためそういったコードをUnityインポート後にRandomな部分の書き換えが発生するのを抑えたいというのが動機です。

早速コードです。

/// <summary>
/// [デバッグ用] Unityとそれ以外の.NET環境で乱数を共通化するためのクラス
/// </summary>
public static class Rand
{
#pragma warning disable IDE1006

#if false // ← これを環境によって自分で切り替える

    //
    // Unityで使用するコード
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// シート値を指定して乱数生成を初期化します。
    /// </summary>
    public static void Init(int seed) => Random.InitState(seed);

    /// <summary>
    /// 指定した整数の範囲内でランダムな値を取得します。maxは値に含まれない。
    /// </summary>
    public static int Next(int min, int max) => Random.Range(min, max);

    /// <summary>
    /// 指定した整数の範囲内でランダムな値を取得します。maxは値に含まれない。
    /// </summary>
    public static int Range(int min, int max) => Random.Range(min, max);

    /// <summary>
    /// 指定した浮動小数の範囲内でランダムな値を取得します。maxは値に含まれます。
    /// </summary>
    public static float Range(float min, int max) => Random.Range(min, max);

    /// <summary>
    /// 0.0f ~ 1.0fまでの範囲でランダムな値を取得します。
    /// </summary>
    public static float Value => Random.value; // .NETの一般的な命名規則との互換用

    /// <summary>
    /// 0.0f ~ 1.0fまでの範囲でランダムな値を取得します。
    /// </summary>
    public static float value => Value;
#else
    //
    // Unity以外の通常のシステムで使用するコード
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    private static Random _r = new Random();

    /// <summary>
    /// シート値を指定して乱数生成を初期化します。
    /// </summary>
    public static void Init(int seed) => _r = new Random(seed);

    /// <summary>
    /// 指定した整数の範囲内でランダムな値を取得します。maxは値に含まれない。
    /// </summary>
    public static int Next(int min, int max) => _r.Next(min, max);

    /// <summary>
    /// 指定した整数の範囲内でランダムな値を取得します。maxは値に含まれない。
    /// </summary>
    public static int Range(int min, int max) => _r.Next(min, max);

    /// <summary>
    /// 指定した浮動小数の範囲内でランダムな値を取得します。maxは値に含まれます。
    /// </summary>
    public static float Range(float min, int max) => throw new NotSupportedException();

    /// <summary>
    /// 0.0f ~ 1.0fまでの範囲でランダムな値を取得します。
    /// </summary>
    public static float Value => (float)_r.NextDouble();

    /// <summary>
    /// 0.0f ~ 1.0fまでの範囲でランダムな値を取得します。
    /// </summary>
    public static float value => Value; // Unityの命名規則の互換用
#endif

#pragma warning restore IDE1006
}

同じシグネチャーのメソッドをUnity用、それ以外用の両方でifdefで区切って定義します。

これで冒頭の「#if false」の部分をUnityならtrue、それ以外ならfalseとして各々のコードから使用します。

環境ごとに中身の実装が違うのでちょっと乱数の出かたが異なり(floatの範囲選択が.NET 環境ではできないですが…)Randクラスを使用する事で冒頭の書き換えるなどの課題が解消できると思います。

【C#】Unityと.NET標準ライブラリの命名規則の違い

.NET標準は「System名前空間内にあるクラス類」を指します。一方のUnityは「UnityEngine名前空間内にあるクラス類」以下にあるオブジェクト群を指します。それぞれ同じC#言語ですが各々の間で大きく命名規則が異なっています。という訳でちょっとまとめてみました。

命名規則の種類

命名規則にはいくつか代表的な形式があります。代表的な形式を以下に挙げます。

Item Description e.g.
Pascal 形式 先頭が大文字で後は小文字 "ItemList", "GameControlelr"
camel 形式 先頭が小文字で語句ごとに大文字 "itemList", "gameController"
snake(スネーク) 形式 全部小文字、語句をアンダースコアでつなぐ "item_list", "game_controller"
CONSTANT 形式 全部大文字、語句をアンダースコアでつなぐ "ITEM_LSIT, "GAME_CONTROLLER"

C#では(というか古くはMSの推奨として)、「snake 形式」と「CONSTANT 形式」は使用しません。従って、他言語では見かけることもある定数を「CONSTANT 形式」で宣言といった文化はありません。enumのメンバーも同様です。

public static class Cosntant
{
    // こういう命名規則は使用しない
    public const int ERROR_CODE = -1;

    // 一般的にこうする
    public const int ErrorCode = -1;
}

最近はIDE上で定数は特別な色で表示されるシーンも多いため命名規則で区別しないでも大丈夫みたいな側面はあると思います。個人的にはそこまで困ったことないですね。

識別子の命名規則

.NET標準とUnityの命名規則の違いを以下にまとめてみました。

種類 .NET標準 Unity e.g
クラス・構造体 Pascal形式 Pascal形式 class Sample { ...
抽象クラス Pascal形式 Pascal形式 abstract class Sample { ...
インターフェース Pascal形式 Pascal形式 interfalce class Sample { ...
列挙型(宣言) Pascal形式 Pascal形式 enum Color { ...
列挙型(メンバー) Pascal形式 Pascal形式 enum Color { Red, Blue...
デリゲート Pascal形式 Pascal形式 delegate void Foo(int, int)
定数(private以外) Pascal形式 Pascal形式 public const string Message
定数(private) Pascal形式 Pascal形式 private const string message
フィールド(private以外) 使わないで camel形式 public int transform
フィールド(private) _camel形式 (*1) _camel形式 (*2) private int [ _transform | m_transform ]
メソッド(private以外) Pascal形式 Pascal形式 public void Foo()
メソッド(private) Pascal形式 Pascal形式 private void Foo()
プロパティ(private以外) Pascal形式 camel形式 public int [ Count|count ] { get; }
プロパティ(private) calem形式 不明 あまり使わない
イベント(private以外) Pascal形式 camel形式 publc event Action [ Clicked|clickd ]
イベント(private) calem形式 不明 滅多に使わない
パラメータ(メソッド引数) calem形式 calem形式 public Foo(int count)
ネームスペース Pascal形式 Pascal形式 namespace UnityEngine, System
  • (*1) 以前からアンダースコアつけるべしでしたが最近スタティックには「s_」, スレッドは「t_」などが追加されて議論を呼んでいしたね。
  • (*2) 「m_」だったり「_」だったり形式は色々あるみたいです

「private以外」の表記は「public」「protected」「internal」「protected internal」「private protected(C#7.2から追加)」の4つを指します。つまり自クラス外に可視性があることを指しています。

リストから分かるかと思いますが、ほぼ Pascal 形式で Unity と .NET での違いは外部公開するフィールドとプロパティくらいです。

この命名規則ですが、private なプロパティとか同じクラスにある常数が Pascal 形式で見分けがつかないのが少し気になります。ただ、最近は IDE が種類ごとに細かく色を付けてくれるので完全に識別不能ではないと思います。

Unity 上で開発を行う場合、基本的にUnityの命名規則に従う方が良いと思いまが、Unityに依存しない汎用的なライブラリ類は.NET標準に寄せるなどの使い分けをしたほうがいいかもしれません。汎用アルゴリズムとしてgithub上にコードを公開する場合は .NET の規約に従ったほうが受け入れられやすいと思います。外部公開のプロパティが小文字のライブラリとか Unity のライブラリ以外で見た事がありません。

その他の規約や書き方

publicフィールドの扱い

Unity では変数をインスペクター上に表示するために public で宣言する事がありますが、やはりインスペクター以外の他のクラスから自由に書き換えできるのでできれば避けたいところです。

// Sample.cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    public Vector3 offset; // インスペクター上で編集したい
}

文字数が多くなって面倒ですが(多人数で作業する場合特に)予期せず書き換えられて自分のクラスの処理(Updateなど)が失敗してしまう事もあり外から書き換えてほしくない場合は、最近は以下のように宣言します。

// Sample.cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    // インスペクター上から編集できるが他のクラスからは見えなくなる
    [SerializeField] private Vector3 offset;
}

また、他のクラスから値を確認したい場合プロパティを経由して値の取得だけできるようにします。

// Sample.cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    [SerializeField] private Vector3 offset;

    // (1) C# 6.0以前のプロパティ書き方(= Unity2018.3以前)
    public Vector3 Offset { get { return this.offset; } }

    // (2) C# 6.0以降の書き方 (= Unity2018.3以降)
    public Vector3 Offset => this.offset;
}

// 短く記述可能なので(2)がおすすめ

インスペクター上に表示しつつ読み書きしたい場合、以下のように記述します。

// Sample.cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    [SerializeField] private Vector3 offset;

    // (1) C# 6.0以前のプロパティ書き方
    public Vector3 Offset
    {
        get { return this.offset; }
        set // 値の設定用のプロパティ宣言
        {
            if(value.x <= 2.0f)
            {
                return; // ★ある数値の場合は設定しないなどの値のチェックができる
            }
            this.offset = value;
        }
    }

    // ★(2) C#7.0 の書き方(get/set指定はC# 7.0が必要)
    // ★単純な値の出し入れなら短く書ける
    public Vector3 Offset { get => this.offset; set => this.offset = value; }

    public Vector3 Offset
    {
        get => this.offset; // ★getだけラムダ式で書くこともできる
        set
        {
            // ★長い場合普通の書き方ができる
            if(value.x <= 2.0f)
            {
                return;
            }
            this.offset = value;
        }
    }
}

thisキーワードの有無

クラスのメンバー指すときに使用する this キーワードですが基本は付けません。

.NET標準 Unity
つけない つけない

実際付け始めると this だらけになると視認性が低下します。(VS の設定はすごく昔は this ありだったような気がしますが(?)引きずって this まみれになってたりするのをたまに見かけます)this キーワードを使用するしないはプロジェクトによって異なると思いますが、基本使用しないと思います(this を使用しない場合、自分のクラスの static メンバーとインスタンスメンバーの区別がつかなくなりますがそれが問題になる事は殆どありません)

特定のケース「引数とクラスのフィールド名が同じ場合 this を付ける」とか「継承元のクラスを明示的に指す必要があり base キーワードを使用する時に this を明示したい」、「メソッドにnew キーワードを使用しているメソッドを base と区別するために明示する」など、一部の場面で base やパラメータと区別するために this が必要になるのでそういった場合 this が必要になります。

個人の場合は好きにしましょう。まぁホビーユースなら面倒なので付けないと思います。

フィールドメンバーの先頭にm_, s_

割と良くあると思いますが以下のような感じです。

// Unity
private Vector3 m_position;
private Transform m_transform;

// .NET
private float _value; // 基本的にアンダースコアを付ける
private static float s_value; // static は先頭に s_

基本的にUnityだとインスペクター上に表示するときは、「m_」 とか先頭の「_(アンダースコア)」は取り外してくれるし UnityEngine 内では使われまくっているので事実上標準です。但し使ってる理由が「メンバー変数がたくさんあって分類するため」だと少し考えちゃいますね。クラスの設計自体がインスペクターの表示に影響を受けることがあるため善し悪しです。

変数名の先頭にアンダースコア

前述の「m_」の事もありますが、これはプロジェクトや個人のポリシーで使用するか決めたほうがいいと思います。

.NET だと先頭に「s_」、「t_」を付けろだとか言っていますがまぁ慣れていないと気持ち悪い、他のプロジェクトと整合が取れない個人の分類法に反するなど色々あると思うので。

但し、プレフィックスは、this キーワードとすこぶる相性が悪いので例えば以下のように併用してしまうとめちゃくちゃです。

private int _count = 10;

public void Foo()
{
    this._count = 20; // thisと併用すると見た目が意味不明になる
}

こういうケースが発生しないように規約は調整する必要があります。

結局どうすればいいの?

他人に公開するつもりのあるコードはファイル先頭に以下を宣言します。

#pragma warning disable // コード内の警告を全て抑制する

// もしくは

#if RELEASE
#pragma warning disable
#endif

という冗談はさておき、特殊な規約を採用しているプロジェクトのコードが自分のプロジェクトに取り込まれた時にめちゃくちゃ警告が出るのは予防できそうですtが。

まぁでも規約は規約なので、守るかかもらないか、スタイルをどうするかは個人の場合は好みで決めて大丈夫です。何が良いという事はないです。

ただ自分の決めたポリシーと既存のコードの規約が違う場合に、Githubの規約にも記載ありましたが他人のコードの方を無理に直そうとするのはやめた方がいいです。普通に時間の無駄 or 反感を買うだけなので既存のスタイルがあるならそ少なくともそのファイル内はそのスタイルを踏襲して表記を統一しましょう。いくつか書き方の引き出しのセットがあって切り替えてくイメージです。

ちなみにゼロから開発するときにプロジェクト内の場所ごとに表記が違うのNGです。チームの場合はガイドラインを設けましょう。必ず。絶対に。ネット上にあるコーディング規約でJava由来のクソみたいなあまり良くないのが見つかりますがそれは採用してはいけません。

規約を決めたら CodeFX や Lint に規約を設定してツールで逐次チェックしているのが安いです(設定は大変だと思いますが最終的に安いです)

参考資料

MSDN、「名前付けガイドライン」:

https://docs.microsoft.com/ja-jp/dotnet/standard/design-guidelines/naming-guidelines

Unityの命名規則のフォーラム:

https://forum.unity.com/threads/c-naming-conventions-for-unity.135617/

.NET Core のコーディングガイドライン

https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md

C#で1次元配列と2次元配列を相互に変換する

タイトルの通り、1次元配列と2次元配列の相互変換を行う処理の紹介です。

考え方

このような2次元配列を

f:id:Takachan:20200229161039p:plain

このような配列に変換することができます。

f:id:Takachan:20200229164959p:plain

実装コード

早速実装例を紹介したいと思います。

ArrayUtilityクラス

配列に対する操作のため以前紹介したArrayUtiityクラスを今回も使用します。

// ArrayUtility.cs

using System;
using System.Collections.Generic;

/// <summary>
/// 配列に対する汎用機能を提供します。
/// </summary>
public static class ArrayUtility
{
    /// <summary>
    /// 指定した2次元配列を1次元配列に変換します。
    /// </summary>
    public static T[] ToOneDimensional<T>(T[,] src)
    {
        int ymax = src.GetLength(0);
        int xmax = src.GetLength(1);
        int len = xmax * ymax;
        var dest = new T[len];

        for (int y = 0, i = 0; y < ymax; y++)
        {
            for (int x = 0; x < xmax; x++, i++)
            {
                dest[i] = src[y, x];
            }
        }
        return dest;
    }

    /// <summary>
    /// 組み込み型のみを対象に2次元配列を1次元配列に変換します。
    /// </summary>
    public static T[] ToOneDimensionalPrimitives<T>(T[,] src)
    {
        int ymax = src.GetLength(0);
        int xmax = src.GetLength(1);
        int len = xmax * ymax;
        var dest = new T[len];

        var size = Marshal.SizeOf(typeof(T));
        Buffer.BlockCopy(src, 0, dest, 0, len * size);
        return dest;
    }

    /// <summary>
    /// 指定した2次元配列を1次元配列に変換します。
    /// <para>T[height, width] 範囲を超える分は切り捨て、不足している分は(T)の初期値になります。</para>
    /// </summary>
    public static T[,] ToTowDimensional<T>(T[] src, int width, int heigth)
    {
        var dest = new T[heigth, width];
        int len = width * heigth;
        len = src.Length < len ? src.Length : len;
        for (int y = 0, i = 0; y < heigth; y++)
        {
            for (int x = 0; x < width; x++, i++)
            {
                if (i >= len)
                {
                    return dest;
                }
                dest[y, x] = src[i];
            }
        }

        return dest;
    }

    /// <summary>
    ///  組み込み型のみを対象に1次元配列を2次元配列に変換します。
    /// <para>T[height, width] 範囲を超える分は切り捨て、不足している分は(T)の初期値になります。</para>
    /// </summary>
    public static T[,] ToTowDimensionalPrimitives<T>(T[] src, int width, int heigth)
    {
        var dest = new T[heigth, width];
        int len = width * heigth;
        len = src.Length < len ? src.Length : len;

        var size = Marshal.SizeOf(typeof(T));
        Buffer.BlockCopy(src, 0, dest, 0, len * size);
        return dest;
    }
}

使い方

使い方はシンプルに変換したい配列を指定し、X, Y のサイズを指定し変換を行います。

// AppMain.cs

using System;
using System.Collections.Generic;

public static void Main(string[] args)
{
    // 1次元配列を宣言
    int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };

    // 2x2 の 2次元配列に変換、はみ出した分は無視される
    var tow = ArrayUtility.ToTowDimensional(array, 2);
    // 0, 1
    // 2, 3

    // 2次元配列を1次元配列に変換
    var one = ArrayUtility.ToOneDimensional(array);
    // 0, 1, 2, 3

    Console.WriteLine(one);
}

ArrayExtension:拡張メソッド版

上記の処理を配列の拡張メソッドとして定義したいと思います。

コード例は以下の通りです。

先ほどのUtilityの処理を中で呼び出すようにしています。

/// <summary>
/// 配列に対する拡張機能を提供します。
/// </summary>
public static class ArrayExtension
{
    /// <summary>
    /// 指定した2次元配列を1次元配列に変換します。
    /// </summary>
    public static T[] ToOneDimensional<T>(this T[,] array)
    {
        ArrayUtility.ToOneDimensional(array);
    }

    /// <summary>
    /// 指定した2次元配列を1次元配列に変換します。
    /// </summary>
    public static T[,] ToTowDimensional<T>(this T[] array, int size)
    {
        ArrayUtility.ToTowDimensional(array, size);
    }
}

使い方

使用方法は先ほどとほぼ同じですが配列のインスタンスを直接指定して処理を呼び出せるようになっていると思います。

// AppMain.cs

using System;
using System.Collections.Generic;

public static void Main(string[] args)
{
    // 1次元配列を宣言
    int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };

    // 2x2 の 2次元配列に変換、はみ出した分は無視される
    var tow = array.ToTowDimensional(2);
    // 0, 1
    // 2, 3

    // 2次元配列を1次元配列に変換
    var one = array.ToOneDimensional();
    // 0, 1, 2, 3

    Console.WriteLine(one);
}

こちらも最初のコードと同じように相互変換することができました。

コードばかりになってしまいましたが以上です。

C#でHexタイルの位置を計算する

今回は6角形のタイルの座標の計算を行うライブラリの紹介をしたいと思います。

計算した位置をいい感じに描画すると以下のような感じに並べることができます。

f:id:Takachan:20200224222537g:plain

HexLayoutクラス:6角形のタイルを並べる

結構強引に位置を計算していますが、指定した位置を中心に同心円状に並べる処理とX, Yで指定した位置を1つ取得する処理の2種類を用意しています。

/// <summary>
/// 2Dの6角形を配置するための位置を計算するクラス
/// </summary>
public class HexLayout
{
    /// <summary>
    /// 同心円状に配置する位置を取得します。
    /// </summary>
    public static IEnumerable<(float x, float y)> GetPosByConcentricCircle(float size, int level)
    {
        float SIZE_X = size;
        float SIZE_Y = SIZE_X * 0.75f;

        // 左上
        for (int i = 0; i < level; i++)
        {
            float xpos = SIZE_X / 2 * level;
            xpos += SIZE_X / 2 * i;
            float ypos = SIZE_Y * level;
            ypos -= SIZE_Y * i;
            yield return (-xpos, ypos);
        }

        // 左
        for (int i = 0; i < level; i++)
        {
            float xpos = SIZE_X * level;
            xpos -= SIZE_X / 2 * 1 * i;
            float ypos = 0;
            ypos -= SIZE_Y * i;
            yield return (-xpos, ypos);
        }

        // 左下
        for (int i = 0; i < level; i++)
        {
            float xpos = SIZE_X / 2 * level;
            xpos -= SIZE_X * i;
            float ypos = SIZE_Y * -level;
            yield return (-xpos, ypos);
        }

        // 右下
        for (int i = 0; i < level; i++)
        {
            float xpos = SIZE_X / 2 * level;
            xpos += SIZE_X / 2 * i;
            float ypos = SIZE_Y * -level;
            ypos += SIZE_Y * i;
            yield return (xpos, ypos);
        }

        // 右
        for (int i = 0; i < level; i++)
        {
            float xpos = SIZE_X * level;
            xpos -= SIZE_X / 2 * 1 * i;
            float ypos = 0;
            ypos += SIZE_Y * i;
            yield return (xpos, ypos);
        }

        // 右上
        for (int i = 0; i < level; i++)
        {
            float xpos = SIZE_X / 2 * level;
            xpos -= SIZE_X * i;
            float ypos = SIZE_Y * level;
            yield return (xpos, ypos);
        }
    }

    /// <summary>
    /// 指定した座標のHexの位置を取得します。
    /// </summary>
    public static (float x, float y) GetHexPos(int x, int y, float size)
    {
        //
        // 以下のような並び順で配置します:
        //
        // (-1,-2) ( 0,-2) ( 1,-2)
        //     ( 0,-1) ( 1,-1)
        // (-1, 0) ( 0, 0) ( 1, 0)
        //     ( 0, 1) ( 1, 1)
        // (-1, 1) ( 0, 1) ( 1, 1)
        //

        float xpos = Mathf.Abs(size * x);
        if (y % 2f != 0)
        {
            xpos -= size / 2.0f;
        }
        float ypos = Mathf.Abs(size * 0.75f * y);

        if (y > 0)
        {
            ypos *= -1;
        }

        return (xpos, ypos);
    }
}

考えかた

同心円計のGetPosByConcentricCircleメソッドは引数のレベルが何週目の円の位置かを表し、例えば2レベルでは以下のように左回りに位置の計算を行います。

f:id:Takachan:20200224223748p:plain

座標指定のGetHexPosは指定したXとYに従って以下座標系で位置を返します。

f:id:Takachan:20200224224128p:plain

これらの結果を受け取ってUnity上でSpriteに位置の指定を行ったものが冒頭のGifになります。

短いですが以上です。

【C#】の1次元配列と多次元配列、リストのアクセス速度の違い

前回の記事で紹介した2次元配列の管理クラスですが中身のデータを「1次元配列を2次元配列扱いする」か「C#固有機能の多次元配列」で行ったときの実行速度に触れましたが今回は実際に速度の違いを計測してみました。

takachan.hatenablog.com

タイトルの通り、2次元配列のデータ格納方式各々のアクセス速度を計測します。

計測対象

2次元配列をC#上で扱うためにはいくつかの実装方法があり、各々のアクセス速度を計測します。対象とするデータ形式は以下の通りです。

Item 説明
int[ ] 1次元配列、データアクセスは int[y * STRIDE + x] で行う
int[ ][ ] 2次元配列、ジャグ配列と呼ぶ。アクセス方法は int[y][x]
int[, ] 2次元配列、C#固有の宣言方法。アクセス方法は int[y, x]
List<List> リストを2次元配列に見立てる。アクセス方法は list[y][x]

確認環境

この実行速度計測は以下環境で確認しています。

  • .NET 7 + VisualStudio2022
  • BenchmarkNet 0.13.10
  • Windows11 22H2
  • Ryzen 5900X 3.7GHz
  • 32GB メモリー

.NET 7 Release ビルドの Exe をコマンドラインから実行して速度を計測しています。

実行結果

先に実行結果を載せておきます。それぞれ 3238 x 3238 の 1048万回アクセスを試行して計測しています。

アクセス方法 int[ ] int[ ][ ] int[, ] List<List>
シーケンシャルアクセス(時間) 2.259ms 4.456ms 5.595ms 7.784ms
シーケンシャル(割合) x1.0 x1.97 x2.47 x3.44
ランダムアクセス(時間) 15.35ms 44.58ms 217.97ms 32.34ms
ランダム(割合) x1.0 x2.9 x14.19 x2.1

総合的に見て1次元配列を(x, y)でアクセスするのが一番よさそうでした。

なんか C# 固有の [, ] という二次元配列はランダムアクセスでめちゃくちゃパフォーマンスが悪いですがこれどうなってるんでしょうね?

あと List クラスが .NET 7以降高速化されてるのでアクセス速度がめちゃくちゃ早くなってるみたいです。動的にサイズを変えるケースがあるなら List でもいいのかもしれません。

実行コード

ここでは速度計測に使用したコードを掲載します。

シーケンシャルアクセス

シーケンシャルアクセスは先頭から末尾までの要素を順にアクセスして要素へのアクセス時間を計測します。

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
[RankColumn]
public class Test1
{
    // 合計1048万グリッド
    const int WIDTH = 3238;
    const int HEIGHT = 3238;

    // (1) 1次元配列を2次元配列扱いする
    int[] _array;
    // (2) 2次元配列(=ジャグ配列)
    int[][] _jagged;
    // (3) 2次元配列(C#固有機能)
    int[,] _csarray;
    // (4) List<T>で2次元配列
    List<List<int>> _listArray;

    [GlobalSetup]
    public void Setup()
    {
        // (1)のデータ構を初期化
        _array = new int[WIDTH * HEIGHT];

        // (2)のデータ構造を初期化
        _jagged = new int[HEIGHT][];
        for (int i = 0; i < WIDTH; i++)
        {
            _jagged[i] = new int[WIDTH];
        }
        
        // (3)のデータ構造を初期化
        _csarray = new int[HEIGHT, WIDTH];
        
        // (4)のデータ構造を初期化
        _listArray = new List<List<int>>();
        for (int i = 0; i < HEIGHT; i++)
        {
            _listArray.Add(new List<int>());
        }
        
        // (1)~(4)のデータ構造に初期値を設定
        var r = new Random();
        for (int y = 0; y < HEIGHT; y++)
        {
            for (int x = 0; x < WIDTH; x++)
            {
                int p = r.Next();
                _array[HEIGHT * y + x] = p;
                _jagged[y][x] = p;
                _csarray[y, x] = p;
                _listArray[y].Add(p);
            }
        }
    }

    // (1) に対するシーケンシャルアクセス
    [Benchmark]
    public void _1_Seq()
    {
        //var temp = new List<int>();
        for (int y = 0; y < HEIGHT; y++)
        {
            for (int x = 0; x < WIDTH; x++)
            {
                int value = _array[y * HEIGHT + x];
                //temp.Add(value);
            }
        }
    }
    // (2) に対するシーケンシャルアクセス
    [Benchmark]
    public void _2_Seq()
    {
        //var temp = new List<int>();
        for (int y = 0; y < HEIGHT; y++)
        {
            for (int x = 0; x < WIDTH; x++)
            {
                int value = _jagged[y][x];
                //temp.Add(value);
            }
        }
    }
    // (3) に対するシーケンシャルアクセス
    [Benchmark]
    public void _3_Seq()
    {
        //var temp = new List<int>();
        for (int y = 0; y < HEIGHT; y++)
        {
            for (int x = 0; x < WIDTH; x++)
            {
                int value = _csarray[y, x];
                //temp.Add(value);
            }
        }
    }
    // (4) に対するシーケンシャルアクセス
    [Benchmark]
    public void _4_Seq()
    {
        //var temp = new List<int>();
        for (int y = 0; y < HEIGHT; y++)
        {
            for (int x = 0; x < WIDTH; x++)
            {
                int value = _listArray[y][x];
                //temp.Add(value);
            }
        }
    }
}

| Method | Mean     | Error     | StdDev    | Median   | Rank | Allocated |
|------- |---------:|----------:|----------:|---------:|-----:|----------:|
| _1_Seq | 2.259 ms | 0.0364 ms | 0.0340 ms | 2.233 ms |    1 |       2 B |
| _2_Seq | 4.456 ms | 0.0063 ms | 0.0059 ms | 4.456 ms |    2 |       4 B |
| _3_Seq | 5.595 ms | 0.0151 ms | 0.0134 ms | 5.593 ms |    3 |       4 B |
| _4_Seq | 7.784 ms | 0.0122 ms | 0.0114 ms | 7.786 ms |    4 |       4 B |

ランダムアクセス

ランダムアクセスは要素内の適用な要素を選択し(1)~(4)全て同じ位置にアクセスし各々の時間を計測します。

[MemoryDiagnoser]
[RankColumn]
public class Test2
{
    const int WIDTH = 3238;
    const int HEIGHT = 3238;

    // (1) 1次元配列を2次元配列扱いする
    int[] _array;
    // (2) 2次元配列(=ジャグ配列)
    int[][] _jagged;
    // (3) 2次元配列(C#固有機能)
    int[,] _csarray;
    // (4) List<T>で2次元配列
    List<List<int>> _listArray;

    // ランダムアクセスの回数
    const int CNT = WIDTH * HEIGHT;
    // ランダムアクセス用のインデックス
    List<(int x, int y)> _randList = new List<(int x, int y)>();

    [GlobalSetup]
    public void Setup()
    {
        // (1)のデータ構を初期化
        _array = new int[WIDTH * HEIGHT];

        // (2)のデータ構造を初期化
        _jagged = new int[HEIGHT][];
        for (int i = 0; i < WIDTH; i++)
        {
            _jagged[i] = new int[WIDTH];
        }

        // (3)のデータ構造を初期化
        _csarray = new int[HEIGHT, WIDTH];

        // (4)のデータ構造を初期化
        _listArray = new List<List<int>>();
        for (int i = 0; i < HEIGHT; i++)
        {
            _listArray.Add(new List<int>());
        }

        // (1)~(4)のデータ構造に初期値を設定
        var r = new Random();
        for (int y = 0; y < HEIGHT; y++)
        {
            for (int x = 0; x < WIDTH; x++)
            {
                int p = r.Next();
                _array[HEIGHT * y + x] = p;
                _jagged[y][x] = p;
                _csarray[y, x] = p;
                _listArray[y].Add(p);
            }
        }

        // ランダムアクセス用の乱数の初期化
        var rand = new Random();
        for (int i = 0; i < CNT; i++)
        {
            _randList.Add((rand.Next(0, WIDTH), rand.Next(0, HEIGHT)));
        }
    }

    // (1) に対するランダムアクセス
    [Benchmark]
    public void _1_Rand()
    {
        //var temp = new List<int>();
        foreach (var (x, y) in _randList)
        {
            int value = _array[y * HEIGHT + x];
            //temp.Add(value);
        }
    }
    // (2) に対するランダムアクセス
    [Benchmark]
    public void _2_Rand()
    {
        //var temp = new List<int>();
        foreach (var (x, y) in _randList)
        {
            int value = _jagged[y][x];
            //temp.Add(value);
        }
    }
    // (3) に対するランダムアクセス
    [Benchmark]
    public void _3_Rand()
    {
        //var temp = new List<int>();
        foreach (var (x, y) in _randList)
        {
            int value = _csarray[y, x];
            //temp.Add(value);
        }
    }
    // (4) に対するランダムアクセス
    [Benchmark]
    public void _4_Rand()
    {
        //var temp = new List<int>();
        foreach (var (x, y) in _randList)
        {
            int value = _listArray[y][x];
            //temp.Add(value);
        }
    }
}

| Method  | Mean      | Error    | StdDev   | Rank | Allocated |
|-------- |----------:|---------:|---------:|-----:|----------:|
| _1_Rand |  15.35 ms | 0.255 ms | 0.226 ms |    1 |       8 B |
| _2_Rand |  44.58 ms | 0.886 ms | 1.055 ms |    2 |      42 B |
| _3_Rand | 217.98 ms | 3.918 ms | 3.473 ms |    4 |     168 B |
| _4_Rand |  82.34 ms | 1.640 ms | 3.600 ms |    3 |      72 B |

長くなりましたが以上です。

【C#】グリッドマップを管理するクラスを作成する

2D のゲーム実装でマップを扱うときは x と y の2次元のマップデータを使うことがありますがこのデータ構造の実装方法を紹介します。

x と y の 2次元のデータですが、中身は 1次元の配列として扱います。なので 1列の配列データを 2次元のグリッドとして扱う方法となります。

こんな感じのイメージの並び順の2次元配列を想像して書いています。

確認環境

以下の環境で動くことを確認しています。

  • VisualStudio2019
  • .NET Framwwork 4.7.2
  • .NET Core 3.1
  • Unity2019.3

実装コード

では早速実装例を紹介したいと思います。

Map2Dクラス

いちおう2次元配列を直接触ることもできますが、メソッド経由の方が脳にやさしいと思います。

各座標に対するよくある操作を定義しています。

(T)には任意の型を指定できるのでオブジェクトを指定することもできます。

/// <summary>
/// 任意の型(T)の2次元配列を表します。
/// </summary>
public class Map2D<T>
{
    //
    // Descriptions
    // - - - - - - - - - - - - - - - - - - - -
    #region...
    //
    // 以下モデルを想定
    //
    //     xi → → → →
    //  yi 00 01 02 03 04
    //  ↓ 10 11 12 13 14
    //  ↓ 20 21 22 23 24
    //  ↓ 30 31 32 33 34
    //  ↓ 40 41 42 43 44
    //
    // もしくはこう
    //
    // ↑ 40 41 42 43 44
    // ↑ 30 31 32 33 34
    // ↑ 20 21 22 23 24
    // ↑ 10 11 12 13 14
    // yi 00 01 02 03 04
    //    xi → → → →
    //
    #endregion

    /// <summary>
    /// このオブジェクトが内部で管理している配列を取得します
    /// (外から内容を操作してもいいけど自己責任)
    /// </summary>
    public T[] Array { get; private set; }

    /// <summary>
    /// マップの横幅を取得します。
    /// </summary>
    public int Width { get; private set; }

    /// <summary>
    /// マップの縦幅を取得します。
    /// </summary>
    public int Height { get; private set; }

    //
    // Operators
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 多次元配列のインデクサーにより値を設定または取得します。
    /// </summary>
    public T this[int xi, int yi] { get => this.GetItem(xi, yi); set => this.SetItem(xi, yi, value); }

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

    /// <summary>
    /// 既定の初期値でオブジェクトを新規作成します。
    /// </summary>
    public Map2D() { }

    /// <summary>
    /// マップの縦・横の大きさを指定してオブジェクトを新規作成します。
    /// </summary>
    public Map2D(int width, int height) => this.Init(width, height);

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

    /// <summary>
    /// 指定した値でオブジェクトを初期化します。
    /// </summary>
    public void Init(int width, int height)
    {
        this.Width = width;
        this.Height = height;
        this.Array = new T[height * width];
    }

    /// <summary>
    /// 指定した2次元配列でオブジェクトを初期化します。
    /// </summary>
    public void Init(T[] src)
    {
        this.Array = src;
        this.Width = src.GetLength(1);
        this.Height = src.GetLength(0);
    }

    /// <summary>
    /// 指定位置にIDを設定します。
    /// </summary>
    public void SetItem(int xi, int yi, T id) => this.Array[yi * this.Width + xi] = id;

    /// <summary>
    /// 指定位置のIDを取得します。
    /// </summary>
    public T GetItem(int xi, int yi) => this.Array[yi * this.Width + xi];

    /// <summary>
    /// <para>
    /// 指定した位置がマップの範囲内かどうかを判定します。
    /// true : 範囲内 / false : 範囲外
    /// </para>
    /// <para>
    /// 基本的にすべてのメソッドは範囲チェックを行わないので指定するxi, yiが範囲内かどうかは
    /// このメソッドを使用して判定もしくは外部でチェックされていることを想定します。
    /// </para>
    /// </summary>
    public bool IsIn(int xi, int yi) => !(xi < 0 || yi < 0 || xi >= this.Width || yi >= this.Height);

    /// <summary>
    /// 指定したYの行要素をすべて列挙します。
    /// </summary>
    public IEnumerable<(int x, int y, T item)> GetRow(int yi)
    {
        for (int x = 0; x < this.Width; x++)
        {
            yield return (x, yi, this.GetItem(x, yi));
        }
    }

    /// <summary>
    /// 指定したXの列要素をすべて列挙します。
    /// </summary>
    public IEnumerable<(int x, int y, T item)> GetColumn(int xi)
    {
        for (int y = 0; y < this.Height; y++)
        {
            yield return (xi, y, this.GetItem(xi, y));
        }
    }

    /// <summary>
    /// 指定したYの行要素に対し述語で一括で処理を行います。
    /// </summary>
    public void ForRow(int yi, Action<int/*xi*/, int/*yi*/, T/*id*/> func)
    {
        foreach ((int x, int y, T id) in this.GetRow(yi))
        {
            func(x, y, id);
        }
    }

    /// <summary>
    /// 指定したYの列要素に対し述語で一括で処理を行います。
    /// </summary>
    public void ForColumn(int xi, Action<int/*xi*/, int/*yi*/, T/*id*/> func)
    {
        foreach ((int x, int y, T id) in this.GetColumn(xi))
        {
            func(x, y, id);
        }
    }

    /// <summary>
    /// 全ての要素を列挙し述語で処理を行います。
    /// </summary>
    public void ForEach(Action<int/*xi*/, int/*yi*/, T/*id*/> func)
    {
        for (int y = 0; y < this.Height; y++)
        {
            for (int x = 0; x < this.Width; x++)
            {
                func(x, y, this.GetItem(x, y));
            }
        }
    }
}

Map2Diクラス

マップチップなどでCSVから読み取ったデータはint型の配列なことが多いと思うので以下のクラスをあらかじめ定義しておきます。

//
// 一番よく使うと思われるので事前に定義しておく
//

/// <summary>
/// <see cref="int"/> 型の2次元配列を表します。
/// 
/// </summary>
public class Map2Di : Map2D<int>
{
    public Map2Di() { }
    public Map2Di(int width, int hegiht) : base(width, hegiht) { }
}

使い方

上記クラスの使い方は以下の通りです。

値の出し入れと、一括処理の方法です。

public static void Main(string[] args)
{
    // (1) 新規に配列を作成
    var map1 = new Map2Di(10, 10);

    // (2) もともとある配列を指定して初期化
    int[,] _m = new int[,]
    {
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
        { 1, 1, 1, 1, 1, 2, 2, 2, 2, 2 },
        { 2, 2, 2, 2, 2, 3, 3, 3, 3, 3 },
        { 3, 3, 3, 3, 3, 4, 4, 4, 4, 4 },
        { 4, 4, 4, 4, 4, 5, 5, 5, 5, 5 },
        { 5, 5, 5, 5, 5, 6, 6, 6, 6, 6 },
        { 6, 6, 6, 6, 6, 7, 7, 7, 7, 7 },
        { 7, 7, 7, 7, 7, 8, 8, 8, 8, 8 },
        { 8, 8, 8, 8, 8, 9, 9, 9, 9, 9 },
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
    };
    var map2 = new Map2Di();
    map2.Init(_m);

    // (3) 管理オブジェクトのサイズを取得する
    Console.WriteLine($"({map2.Width}, {map2.Height})");

    // (4) 値の出し入れ
    map2.SetItem(1, 1, 999);
    int item = map2.GetItem(1, 1);

    // (5-1) 1行目のデータを全部取得
    foreach ((int x, int y, int id) p in map2.GetRow(1))
    {
        Console.WriteLine($"({p.x}, {p.y})={p.id}");
    }
    // (5-2) 1行目のデータに対して一括処理
    map2.ForRow(1, (x, y, id) => Console.WriteLine($"({x}, {y})={id}"));

    // (6-1) 1列目のデータを全部取得
    foreach ((int x, int y, int id) p in map2.GetColumn(1))
    {
        Console.WriteLine($"({p.x}, {p.y})={p.id}");
    }
    // (6-2) 1列目のデータに対して一括処理
    map2.ForColumn(1, (x, y, id) => Console.WriteLine($"({x}, {y})={id}"));

    // (7) 全部のデータを列挙
    map2.ForEach((x,y,id)=> Console.WriteLine($"({x}, {y})={id}"));
}

【説明】管理クラスの必要性

2次元配列で実装しているので、この配列にアクセスするときはカッコ内に指定する値はXとYが逆になっています(大抵の人はそうすると思います…

そもそも2次元配列なら管理クラスは必要無いのかもしれませんが、インデックスに指定する順序がY→Xの順のため稀にX→Yの順で指定してアクセス位置を誤る場合があるので誤操作防止の意味があります。

public T GetItem(int xi, int yi)
{
    // YとXの指定は逆、X→Yの順序で指定してアクセス間違いしないようにしている
    return array[y, x];
}

例えば以下のイメージでデータが並んでるときに

上記の色のついているグリッドへのアクセス方法は以下のように直観的に操作できるようになります。

var map2 = new Map2Di();
// ...(中略)...

// 赤い場所のデータを取得
int item1 = map2.GetItem(1, 2);

// 青い場所のデータを取得
int item2 = map2.GetItem(3, 5);

また配列に対する汎用操作をクラスに追加したい場合もクラスに機能追加すれば良いのでクラス化しておけば何かと便利です。

【余談】何故1次元配列で実装するのか?

単純に一番アクセス速度が速いからです。1次元配列を二次元配列として利用するにはインデックスを毎回計算しないといけませんがその計算をしてもほかのデータ形式より実行速度で有利だからです。

速度については以下の記事にまとめました。

takachan.hatenablog.com

上記の気の通りランダムアクセス、シーケンシャルアクセスで最大速度が得られます。C#の2次元配列表現は1次元配列を2次元配列として扱うより速度が低下します。

こういう基本クラスはアクセス頻度が結構高いと思うので処理コストが軽いほうがよいという理由から1次元配列を2次元配列のように扱っています。

以上です。