- 2021/09/16 【Unity】【C#】ガンマ(Gamma, sRGB) - リニア(Linear) 値の相互変換
- 2021/07/23 【C#】文字列 → float (浮動小数点) 変換でエラーが出るときは…
- 2021/07/11 【C#】配列やコレクションの IsNullOrEmpty
- 2021/06/13 【C#】HashSet や Dictionary の AddRange
- 2021/05/17 【C#】配列 や List の TryGetValue
« prev next »
【Unity】【C#】ガンマ(Gamma, sRGB) - リニア(Linear) 値の相互変換 
2021/09/16 Thu [edit]
そろそろ UniVRM をアップデートしたいなぁ、と思ったけど、とうとう UniVRM0.79 以降はカラースペース(Color Space)をリニア(Linear)に統一するらしく、インポートしたら強制的に変更されるようになってしまった。
VRM Live Viewer は3年前、Unity2017初期の頃からはじまったのだが、CRS ステージは Unity4, Crystal ステージは Unity5 時代のものを流用してるんだよね。なので、ガンマ(Gamma)スペースなのだ。
まぁ、正直言って、特に Unity2018 以降の機能は Linear 依りになってきたので(なぜか今でも Unity のデフォは Gamma だが)、いずれ移行したいとは思ってたんだけどね。
しかし、さすがにマイナーバージョンアップデートで、前後の互換性がないのはおかしいし(不具合にしか見えないため)、メジャーアプデとして考えていた。なので急遽移行を迫られる形になってしまった。でも Color Space 変更って、プロジェクト素材全てに影響するので、とてもじゃないけどすぐにはできないんだよね(見た目がかなり変わる)。
なので、自動化できるところはスクリプトで変換などするために、色々調べることになった。これはそのメモ。ほぼ出典そのままだが、汎用的に使えるので掲載しておこう。
■汎用的な値(0~1f:正規化した値)での変換
●定義からの厳密なコード
●高速化近似式 (2.2乗, 1/2.2乗) での変換
●シンプルな3次の高速近似式での変換
■シェーダ(.shader 等)での利用
(※) Unity 2019.4 / Windows10(x64) で確認
■Unity の Color, Mathf での変換
ちなみに、Unity では Color 構造体が linear, gamma というプロパティがあり、Mathf には Mathf.GammaToLinearSpace, Mathf.LinearToGammaSpace という関数があるので、簡単に変換できる。例えば以下のようにできる。
using UnityEngine;
//ガンマ→リニア
var gammaColor = new Color(0.3f, 0.4f, 0.5f);
var linearColor = gammaColor.linear; //→ RGBA(0.073, 0.133, 0.214, 1.000)
var linearValue = Mathf.GammaToLinearSpace(0.5f); //→ 0.2140411ff
//リニア→ガンマ
var linearColor = new Color(0.073f, 0.133f, 0.214f);
var gammaColor = linearColor.gamma; //→ RGBA(0.300, 0.400, 0.500, 1.000)
var gammaValue = Mathf.LinearToGammaSpace(0.2140411f); //→ 0.5f
これで事足りてしまう場合は良いのだが、もう少しつっこんだ実装をしたいとか、高速化した近似式を使いたいというときもあるだろう。それらは以降のようになる。
■汎用的な値(0~1f:正規化した値)での変換
●定義からの厳密な Gamma - Linear 相互変換 (0~1f:正規化した値での計算)
using UnityEngine;
/// <summary>
/// ガンマ(sRGB)→リニア (0.0-1.0:正規化した値) 色空間変換 (厳密なコード)
/// (参考)
/// https://en.wikipedia.org/wiki/SRGB
/// https://tech.cygames.co.jp/archives/2339/
/// </summary>
/// <param name="gamma">0.0-1.0</param>
/// <returns>0.0-1.0</returns>
public static float ToLinearValueStrict(this float gamma)
{
if (gamma <= 0.04045f)
{
return gamma / 12.92f;
}
else
{
return Mathf.Pow((gamma + 0.055f) / 1.055f, 2.4f);
}
}
/// <summary>
/// リニア→ガンマ(sRGB) (0.0-1.0:正規化した値) 色空間変換 (厳密なコード)
/// (参考)
/// https://en.wikipedia.org/wiki/SRGB
/// https://tech.cygames.co.jp/archives/2339/
/// </summary>
/// <param name="linear">0.0-1.0</param>
/// <returns>0.0-1.0</returns>
public static float ToGammaValueStrict(this float linear)
{
if (linear <= 0.0031308f)
{
return linear * 12.92f;
}
else
{
return 1.055f * Mathf.Pow(linear, 0.4166666f) - 0.055f; //0.4166666f = 1/2.4
}
}
ここでは sRGB とガンマを同等に扱っているが、実際にはガンマ補正はディスプレイや機器などで値が違うようなので、あくまで一般的な仕様に合わせたものだと思って欲しい。
式は以下の参考資料そのままだ。定義は Wikipedia に掲載されている。
(定義) sRGB (From sRGB to CIE XYZ, From CIE XYZ to sRGB)
(参考:式) 物理ベースレンダリング -リニアワークフロー編
●高速化近似式 (2.2乗, 1/2.2乗) での変換
また、式の参考ページにも記載されているが、高速化用の近似式は以下のようになる。
●高速化近似式 Gamma - Linear 相互変換 (0~1f:正規化した値での計算)
using UnityEngine;
/// <summary>
/// ガンマ(sRGB)→リニア (0.0-1.0:正規化した値) 色空間変換 (高速化近似式: 2.2乗)
/// (参考) https://tech.cygames.co.jp/archives/2339/
/// </summary>
/// <param name="gamma">0.0-1.0</param>
/// <returns>0.0-1.0</returns>
public static float ToLinearValueFast(this float gamma)
{
return Mathf.Pow(gamma, 2.2f); //or 2.233333f
}
/// <summary>
/// リニア→ガンマ(sRGB) (0.0-1.0:正規化した値) 色空間変換 (高速化近似式: 1/2.2乗)
/// (参考) https://tech.cygames.co.jp/archives/2339/
/// </summary>
/// <param name="linear">0.0-1.0</param>
/// <returns>0.0-1.0</returns>
public static float ToGammaValueFast(this float linear)
{
return Mathf.Pow(linear, 0.45454545f); //1/2.2
}
なぜそうなるのかというのは、以下を参照すると良いだろう。とてもわかりやすく解説されている。
(参考資料)
・ガンマ補正のうんちく
・分かる!リニアワークフローのコンポジット
まぁ端的に言うと、曲線が似てれば、値も似るということだね(笑)。英語では「Magic Number」みたいにも言われている。
ちなみに、冒頭に書いた Unity の Color.linear や Color.gamma, Mathf.GammaToLinearSpace, Mathf.LinearToGammaSpace と値を照合してみたら、「~Strict」の関数(厳密なコード)はほぼ同じで、「~Fast」(高速化近似式)の方は6割くらい一致するようだ(※ただし、誤差を甘めで、小数点3ケタくらいで比較した場合。べき乗を 2.233333f にすると 67%くらい)。しかし見た目では、静止画ならともかく、動いてる絵なら見分けが付かないくらいだ。
また、これらの式をシェーダに使っている例もある。というより、近いものを Unity の PostProcessing で実装されているらしい。最後に抜粋してるので、興味があったら見てみると良い。
●シンプルな3次の高速近似式での変換
また、下記のシェーダの計算を見ていると、2.2乗の近似式は単純に2乗(c * c)にされているが、もう1つ近似式があるね。コメントの ref の先に解説もあるが、なるほど計算負荷が低いらしい(基本的に平方根やべき乗などは、単純な掛け算や足し算より負荷が高いため)。
もう1つの高速化近似式を C# で書いてみよう(そのまま)。
●シンプルな3次の高速近似式での Gamma → Linear 変換 (0~1f:正規化した値での計算)
using UnityEngine;
/// <summary>
/// ガンマ(sRGB)→リニア (0.0-1.0:正規化した値) 色空間変換 (高速化近似式: シンプルな3次)
/// (参考) http://chilliant.blogspot.com.au/2012/08/srgb-approximations-for-hlsl.html?m=1
/// </summary>
/// <param name="gamma">0.0-1.0</param>
/// <returns>0.0-1.0</returns>
public static float ToLinearValueFastCubic(this float gamma)
{
return gamma * (gamma * (gamma * 0.305306011f + 0.682171111f) + 0.012522878f);
}
最初の高速近似式(2.2乗)のコード内のコメントに「or 2.233333f」と書いてあるのは、こちらの資料にあったからだ。
実際に試してみると、こちらも動いてる分には、ぱっと見た目わからない。平方根使うより計算負荷が軽いので、Unity も採用しているのだろう。これは助かる。
(参考資料) sRGB Approximations for HLSL
■シェーダでの利用
以下のコードは実際に Unity の PostProcessing で使われているようだ。そのまま抜粋させて頂いた。
●シェーダ内 (.shader 等)での Gamma - Linear 相互変換 (0~1f:正規化した値での計算)
// sRGB transfer functions
// Fast path ref: http://chilliant.blogspot.com.au/2012/08/srgb-approximations-for-hlsl.html?m=1
//
half SRGBToLinear(half c)
{
#if USE_VERY_FAST_SRGB
return c * c;
#elif USE_FAST_SRGB
return c * (c * (c * 0.305306011 + 0.682171111) + 0.012522878);
#else
half linearRGBLo = c / 12.92;
half linearRGBHi = PositivePow((c + 0.055) / 1.055, 2.4);
half linearRGB = (c <= 0.04045) ? linearRGBLo : linearRGBHi;
return linearRGB;
#endif
}
half3 SRGBToLinear(half3 c)
{
#if USE_VERY_FAST_SRGB
return c * c;
#elif USE_FAST_SRGB
return c * (c * (c * 0.305306011 + 0.682171111) + 0.012522878);
#else
half3 linearRGBLo = c / 12.92;
half3 linearRGBHi = PositivePow((c + 0.055) / 1.055, half3(2.4, 2.4, 2.4));
half3 linearRGB = (c <= 0.04045) ? linearRGBLo : linearRGBHi;
return linearRGB;
#endif
}
half4 SRGBToLinear(half4 c)
{
return half4(SRGBToLinear(c.rgb), c.a);
}
half LinearToSRGB(half c)
{
#if USE_VERY_FAST_SRGB
return sqrt(c);
#elif USE_FAST_SRGB
return max(1.055 * PositivePow(c, 0.416666667) - 0.055, 0.0);
#else
half sRGBLo = c * 12.92;
half sRGBHi = (PositivePow(c, 1.0 / 2.4) * 1.055) - 0.055;
half sRGB = (c <= 0.0031308) ? sRGBLo : sRGBHi;
return sRGB;
#endif
}
half3 LinearToSRGB(half3 c)
{
#if USE_VERY_FAST_SRGB
return sqrt(c);
#elif USE_FAST_SRGB
return max(1.055 * PositivePow(c, 0.416666667) - 0.055, 0.0);
#else
half3 sRGBLo = c * 12.92;
half3 sRGBHi = (PositivePow(c, half3(1.0 / 2.4, 1.0 / 2.4, 1.0 / 2.4)) * 1.055) - 0.055;
half3 sRGB = (c <= 0.0031308) ? sRGBLo : sRGBHi;
return sRGB;
#endif
}
half4 LinearToSRGB(half4 c)
{
return half4(LinearToSRGB(c.rgb), c.a);
}
内容的には、厳密なコードと高速化近似式 (2.2乗 や 1/2.2乗) に同じか近いね。プリプロセッサの「USE_VERY_FAST_SRGB」や「USE_FAST_SRGB」で実行速度を選択できるらしい。
PositivePow は以下の参考資料を(LIGHT11)見て欲しい。正の値しか使わないのなら、ただの pow() でも可能のようだ。
(参考)
・PostProcessing/PostProcessing/Shaders/Colors.hlsl (Unity の PostProcessing 内)
・【Unity】シェーダにおける値のリニア <-> sRGB変換関数 (LIGHT11:PositivePow 掲載)
●PositivePow (負の値にしないべき乗) 抜粋
#define FLT_EPSILON 1.192092896e-07
// Using pow often result to a warning like this
// "pow(f, e) will not work for negative f, use abs(f) or conditionally handle negative values if you expect them"
// PositivePow remove this warning when you know the value is positive and avoid inf/NAN.
float PositivePow(float base, float power)
{
return pow(max(abs(base), float(FLT_EPSILON)), power);
}
float2 PositivePow(float2 base, float2 power)
{
return pow(max(abs(base), float2(FLT_EPSILON, FLT_EPSILON)), power);
}
float3 PositivePow(float3 base, float3 power)
{
return pow(max(abs(base), float3(FLT_EPSILON, FLT_EPSILON, FLT_EPSILON)), power);
}
float4 PositivePow(float4 base, float4 power)
{
return pow(max(abs(base), float4(FLT_EPSILON, FLT_EPSILON, FLT_EPSILON, FLT_EPSILON)), power);
}
結局、どれを使うかはケースバイケースだと思うが、実装を知ってると色々応用が効くので知っておいて損は無い。例えば、ガンマ補正の値は 2.2乗 みたいになってるが(sRGBディスプレイの現在のデファクトスタンダードであり、過去には色々値が異なっていたらしい)、これを引数をして変更すると、見た目も変わる。これを応用してシェーダの方で Properties に入れておくと、インスペクタで変化の加減を調整できるので、とても便利だ。
実際、VRM Live Viewer のプロジェクト内の素材を Linear 用に調整している最中だが、透過の無いテクスチャなどは、マテリアルの調整やいっそ PhotoShop でテクスチャの色調補正すれば、Gamma のときと同じ感じになるので事足りる。しかし、透過や反射するようなマテリアル(色や光の合成が行われるマテリアル)などは、どんなに調整しても Gamma のときと同じようにはならない(参考資料の「加算/合成が変わる」がわかりやすい)。そういう場合は、シェーダに変換式を入れないと、それっぽくはならないようだ(それでも完全には無理だが)。
(参考資料)
・分かる!リニアワークフローのコンポジット
(関連記事)
【Unity】Standard Assets の Flare は Gamma 用だった?
【Unity】色形式:Unity の Color と Android の ARGB(int32) の相互変換をする
【Unity】【C#】Quality (グラフィック品質) を文字列で取得/設定する
【Unity】【C#】画面解像度とアクペクト比(整数)を求める
【HTML】HTMLカラー名・カラーコード表
- 関連記事
category: Unity
thread: ゲーム開発
janre: コンピュータ
tag: Unityリファレンス C#ライブラリ グラフィックス 算術関数 シェーダ【C#】文字列 → float (浮動小数点) 変換でエラーが出るときは… 
2021/07/23 Fri [edit]
もう随分前の話になるが、フランス語圏の人から「VRM Live Viewer を使いたいのだが、起動ができない」というメッセージが来た。
とりあえずアプリのバージョンと状況を聞いて確認してみたが、普通に起動できる。念の為、BOOTH にも出しているので DL して試したが、やはり同じ結果だ。
しばらく何の事か全く見当が付かなかったのだが、根気よくスクショを送って貰ったり、動画を見せて貰ったりしているうちに、環境の違い(OS)くらいしかもう無いと意見が一致したので、彼は英語を、私はフランス語をインストールして OS を再起動してみた。
そしたらどうだ。彼は「起動に成功した」と言い、私は彼が言っていたように起動に失敗した。
原因は起動時に読み込まれる BVH ファイルで、そのファイルはテキストで書かれていて、小数点は "."(ドット) で書かれている。実はフランス語、アイスランド語など特定の言語圏では、小数点は "," (カンマ) で書くので、気の利いた OS (笑)がエラーを吐いていたのだ。
かくして、ライブラリ作者に不具合報告をして、今ではそのフランスの VTuber もめでたく利用して貰っている。
・特定の言語環境下において、BVHロードに失敗する
まぁ、そんなこんなで「文字列を float (浮動小数点)に変換するなら、拡張メソッドを作っておいた方が良いよ」(※使うなら double 等も) という事で、簡単なものを紹介しておこう。
●フランス語、アイスランド語など特定の言語圏でも対応できる 文字列 → float (浮動小数点) 変換
using System;
using System.Globalization;
public static class Extensions //名前は任意
{
/// <summary>
/// 文字列 → float 変換 (CultureInfo.InvariantCulture 指定)
/// </summary>
/// <param name="s">変換元文字列</param>
/// <returns>変換後の float (失敗は 0)</returns>
public static float ToFloat(this string s)
{
if (float.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var value))
{
return value;
}
return 0f;
}
/// <summary>
/// 文字列 → float 変換 (CultureInfo.InvariantCulture 指定)
/// </summary>
/// <param name="s">変換元文字列</param>
/// <param name="result">変換成功時 = float 値 / 変換失敗時 = 0</param>
/// <returns>true = 変換成功 / false = 変換失敗</returns>
public static bool TryParseFloat(this string s, out float result)
{
if (float.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var value))
{
result = value;
return true;
}
result = 0;
return false;
}
}
●メインコード例
using System;
var s = "1.234";
Console.WriteLine("ToFloat : " + s.ToFloat());
if (s.TryParseFloat(out var result))
{
Console.WriteLine("TryParseFloat : " + result);
}
else
{
Console.WriteLine("TryParseFloat failed");
}
TryParseFloat : 1.234
結果だけ見ると何て事ないのだが、この関数なら、フランス語、アイスランド語など特定の言語圏でもエラーにはならない。
ちなみに、float 変換のオプションに使っている「CultureInfo.InvariantCulture」は「カルチャに依存しない」(無視する) ものらしい。要するに OS の言語とは関係なく、常に一定(英語圏)で変換されるようだ。むしろそっちの方がデフォだと有り難いのだが…(例えば文字列比較の Equals 等もデフォだとカルチャが効くので言語によって結果が異なる可能性がある)。
必要なら double 等、他の浮動小数点も作っておいた方が良いだろう。
動作確認するには OS にフランス語を入れて、OS の言語設定でフランス語に設定した後で、OS を再起動して確かめるしかないが、特に最近はアプリが世界中で使われるような時代になったので、これは常備した方が良いだろう。実際私もフランスの人から報告貰うまでは考えもしなかった。
実は、不具合報告って開発者にとっては非常に有用な情報なので「なんか動作がおかしいな…」と思ったら、連絡してあげると良い。我々はバグ取りのエキスパートでもあるので、すぐに原因究明から修正案まで出せると思う。
実際、この不具合(特定の言語環境下において、BVHロードに失敗する) はライブライリ元の UniVRM が抱えていた問題だったので、これが Fix されると、フランス圏の人もアプリや VRM が利用できるようになったわけだしね(少なくとも BVH を使うアプリはそれまでフランス圏では利用できなかったハズ:3tene とかも BVH 機能入ってた気がする)。
単純に不具合が直ると自分も幸せだし、他の多くの利用者も幸せになれるしね(笑)。
(関連記事)
【Unity】【C#】StartsWith, EndsWith が遅いのなら、Equals も遅いのか?
【Unity】【C#】範囲を指定できる Mathf.Repeat
【C#】【Unity】enum 型と string, int 型の相互変換など
【Unity】色形式:Unity の Color と Android の ARGB(int32) の相互変換をする
- 関連記事
-
-
【C#】クラスのフィールド名を文字列の配列で取得する
-
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
-
【C#】【Unity】enum 型と string, int 型の相互変換など
-
【C#】文字列 → float (浮動小数点) 変換でエラーが出るときは…
-
【C#】配列やコレクションの IsNullOrEmpty
-
category: C#
thread: プログラミング
janre: コンピュータ
tag: C#ライブラリ C#リファレンス 算術関数【C#】配列やコレクションの IsNullOrEmpty 
2021/07/11 Sun [edit]
ああ、そう言えば「配列 や List の TryGetValue」やったのに、もっとよく使うもの忘れてたな~、と思い出したので、とても簡単なものだが、あると非常に便利ものなので一応書いておこう。
これは文字列で string.IsNullOrEmpy() をよく使うので、同じような感覚で、配列や List 等が null または 空 (Count = 0) でないかを調べるものがあれば、便利なのにな~と思ったので、昔作ったものだ。実際にあまりに頻繁に使うので、標準関数かと思い違いしてしまうほどだ(笑)。
まぁたぶん、皆似たようなものを作ってる気がするが、まだ勉強しはじめたばかりの人もいるかもなので(面白いことに、もう随分たくさんの記事を書いたが、上級者向けより、初心者向けの記事の方がアクセスは多いものなのだ)、役に立つかはわからないが、とりあえず書いておこう。
●配列やコレクションの IsNullOrEmpty
using System;
using System.Collections.Generic;
public static class Extensions //名前は任意
{
/// <summary>
/// コレクションが null または Count = 0 か?
/// 2021/07/11 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-399.html
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="collection">調べるコレクション</param>
/// <returns>true : null または Count = 0</returns>
public static bool IsNullOrEmpty<T>(this ICollection<T> collection)
{
return collection == null || collection.Count == 0;
}
/// <summary>
/// 配列が null または Length = 0 か?
/// 2021/07/11 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-399.html
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="array">調べる配列</param>
/// <returns>true : null または Length = 0</returns>
public static bool IsNullOrEmpty<T>(this T[] array)
{
return (array == null || array.Length == 0);
}
}
●メインコード例
using System;
using System.Collections.Generic;
var emptyList = new List();
var list = new List() { 1, 2, 3 };
Console.WriteLine("emptyList : " + emptyList.IsNullOrEmpty());
Console.WriteLine("list : " + list.IsNullOrEmpty());
var emptyDic = new Dictionary();
var dic = new Dictionary()
{
{ "hoge", 100 },
{ "fuga", 200 },
};
Console.WriteLine("emptyDic : " + emptyDic.IsNullOrEmpty());
Console.WriteLine("dic : " + dic.IsNullOrEmpty());
var emptySet = new HashSet();
var set = new HashSet() { "hoge", "fuga" };
Console.WriteLine("emptySet : " + emptySet.IsNullOrEmpty());
Console.WriteLine("set : " + set.IsNullOrEmpty());
var emptyArray = new string[0];
var array = new string[] { "apple", "banana", "candy" };
Console.WriteLine("emptyArray : " + emptyArray.IsNullOrEmpty());
Console.WriteLine("array : " + array.IsNullOrEmpty());
list : False
emptyDic : True
dic : False
emptySet : True
set : False
emptyArray : True
array : False
これも本当になんてことないものなんだけどね。できればこれも標準関数に欲しいくらい(笑)。
私はこれに NOT値(反転値)を求める IsExist() みたいな定義も加えて、よく使っている。
●!IsNullOrEmpty() (反転値) を返す IsExist()
//null でない、かつ Count > 0
public static bool IsExist(this ICollection collection) => !collection.IsNullOrEmpty();
//null でない、かつ Length > 0
public static bool IsExist(this T[] array) => !array.IsNullOrEmpty();
これは「if (!list.IsNullOrEmpty())」みたいなものを「if (list.IsExist())」と「!」を入れないで済むので、少しわかりやすくなるので重宝してる。まぁ、日本語でも「〇〇でなくはない」みたいな否定の否定って意味がわかりずらいからね。その辺は好みで(笑)。
(関連記事)
【C#】配列 や List の TryGetValue
【Unity】【C#】配列・リストのシャッフル
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
【C#】配列やListなどの中身(要素)を見る拡張メソッド Dump
【C#】HashSet や Dictionary の AddRange 
2021/06/13 Sun [edit]
前回に引き続き「標準関数にあったら良いな」シリーズ(笑)。
List には後から、要素を複数追加する AddRange() というメソッドがあるが、残念ながら、HashSet や Dictionary には無い。しかし、意外と複数の要素を追加したいときってあるんだよね。そんな時に便利な拡張メソッド。
●HashSet, Dictionary の AddRange
using System;
using System.Collections.Generic;
public static class Extensions //名前は任意
{
/// <summary>
/// 複数の要素を追加する(同じ値は上書きとなる)
/// ※null であっても、必ず生成される
/// 2021/06/13 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-398.html
/// </summary>
/// <param name="set">追加先</param>
/// <param name="collection">追加ソース</param>
/// <returns></returns>
public static HashSet<T> AddRange<T>(this HashSet<T> set, IEnumerable<T> collection)
{
if (set == null)
set = new HashSet<T>();
if (collection != null)
{
foreach (var item in collection)
set.Add(item);
}
return set;
}
/// <summary>
/// 複数のキーと値ペアを追加する(同じキーは上書きとなる)
/// ※null であっても、必ず生成される.
/// 2021/06/13 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-398.html
/// </summary>
/// <param name="dst">追加先</param>
/// <param name="src">追加ソース</param>
/// <returns></returns>
public static Dictionary<K, V> AddRange<K, V>(this Dictionary<K, V> dst, Dictionary<K, V> src)
{
if (dst == null)
dst = new Dictionary<K, V>();
if (src != null)
{
foreach (var item in src)
dst[item.Key] = item.Value;
}
return dst;
}
}
●メインコード例
using System;
using System.Collections.Generic;
using System.Linq;
var aSet = new HashSet<int>(Enumerable.Range(0, 5));
Console.WriteLine("Before : " + aSet.Dump());
var bSet = new HashSet<int>(Enumerable.Range(10, 5));
aSet.AddRange(bSet);
Console.WriteLine("After : " + aSet.Dump());
var cSet = new HashSet<int>(aSet).AddRange(bSet);
Console.WriteLine("c = a + b : " + cSet.Dump());
var aDic = new Dictionary<int, string>()
{
{ 0, "a" },
{ 1, "b" },
{ 2, "c" },
};
Console.WriteLine("Before : " + aDic.Dump());
var bDic = new Dictionary<int, string>()
{
{ 10, "x" },
{ 11, "y" },
{ 12, "z" },
};
aDic.AddRange(bDic);
Console.WriteLine("After : " + aDic.Dump());
var cDic = new Dictionary<int, string>(aDic).AddRange(bDic);
Console.WriteLine("c = a + b : " + cDic.Dump());
After : [0, 1, 2, 3, 4, 10, 11, 12, 13, 14]
c = a + b : [0, 1, 2, 3, 4, 10, 11, 12, 13, 14]
Before : [[0, a], [1, b], [2, c]]
After : [[0, a], [1, b], [2, c], [10, x], [11, y], [12, z]]
c = a + b : [[0, a], [1, b], [2, c], [10, x], [11, y], [12, z]]
要素をダンプしている Dump() は以前の記事からコピペして欲しい。
少し標準関数の List.AddRange() と違うのは、void を返してるのではなく、コレクションを返してる所かな。こうすると、メソッドチェーンみたいにできるので、new と同時にいくつかの別コレクションを追加できたりするので、便利だ。
とても簡単なものだけど、これもあると非常に有用なんだよね。私は結構大きなシステムを自作してるけど、大きくなるほど、こういった便利関数は欠かせなくなる(毎回似たような処理を書くのは大変な物量になるため)。実はプログラミングってこういう小さな工夫の積み重ねなんだよね(笑)。
「これくらいのものなら、LINQ で書けばいいんじゃないの?」と思うかも知れないが、私はあまりオススメしない。というのは LINQ はそれなりにコストがかかるのと、GC を発生しやすいので、頻繁に使われるかも知れない拡張メソッド等は、割とベタな書き方をした方が速いし、GC も制御しやすいからだ。まぁ、string 系の処理はどのみち GC が発生するので使うこともあるが(しかし、できる限り StringBuilder を使う方が良い)、LINQ はどちらかというと、ランタイムで1回きりの初期化処理とか、頻繁には使わない処理用かな。特に Unity のような毎フレーム更新するような処理には気をつけて使った方が良いね。
・コストのかかる操作を避ける
(関連記事)
【C#】配列 や List の TryGetValue
【C#】配列やListなどの中身(要素)を見る拡張メソッド Dump
【Unity】【C#】配列・リストのシャッフル
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
- 関連記事
-
-
【C#】配列 や List の TryGetValue
-
【C#】配列やコレクションの IsNullOrEmpty
-
【C#】HashSet や Dictionary の AddRange
-
【C#】文字列 → float (浮動小数点) 変換でエラーが出るときは…
-
【C#】最大公約数を求める/分数の約分をする(ユークリッドの互除法)
-
category: C#
thread: プログラミング
janre: コンピュータ
tag: C#ライブラリ C#リファレンス 配列操作 連想配列【C#】配列 や List の TryGetValue 
2021/05/17 Mon [edit]
今回はとても簡単なもので、意外と便利なもの。
配列や List を扱う上で注意すべき点は、インデクス範囲外(Index was out of range) と null かどうかだが、毎回 null チェックや範囲外チェックをやるのは少し面倒である。
Dictionary ではキーを用いて値の取得を試みる TryGetValue があるが、これと同じ様なものがあったら便利なのにな~、と思ったので作ってみた。
●配列, List の TryGetValue
using System;
using System.Collections.Generic;
public static class Extensions //名前は任意
{
/// <summary>
/// 配列 の TryGetValue
/// ・配列自体が null or インデクスが範囲外のとき false
/// 2021/05/17 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-397.html
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
/// <param name="array">対象の配列</param>
/// <param name="idx">取得するインデクス</param>
/// <param name="value">取得成功したときの値</param>
/// <returns>true = 取得成功</returns>
public static bool TryGetValue<T>(this T[] array, int idx, out T value)
{
value = default(T);
if (array == null || idx < 0 || array.Length <= idx)
return false;
value = array[idx];
return true;
}
/// <summary>
/// List の TryGetValue
/// ・List 自体が null or インデクスが範囲外のとき false
/// 2021/05/17 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-397.html
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
/// <param name="list">対象の List</param>
/// <param name="idx">取得するインデクス</param>
/// <param name="value">取得成功したときの値</param>
/// <returns>true = 取得成功</returns>
public static bool TryGetValue<T>(this List<T> list, int idx, out T value)
{
value = default(T);
if (list == null || idx < 0 || list.Count <= idx)
return false;
value = list[idx];
return true;
}
}
●メインコード例
using System;
using System.Collections.Generic;
using System.Linq;
int max = 50;
var array = Enumerable.Range(0, max).ToArray(); //0~49 まで
var list = Enumerable.Range(0, max).ToList(); //0~49 まで
int idxA = 25;
if (array.TryGetValue(idxA, out var resultA))
{
Console.WriteLine($"result value (Array) = {resultA}");
}
else
{
Console.WriteLine($"idx = {idxA} : Index was out of range or null (Array)");
}
int idxL = 50;
if (list.TryGetValue(idxL, out var resultL))
{
Console.WriteLine($"result value (List) = {resultL}");
}
else
{
Console.WriteLine($"idx = {idxL} : Index was out of range or null (List)");
}
//var res = list[100]; //Index was out of range
idx = 50 : Index was out of range or null (List)
本当になんてことないものなんだけどね。できれば標準関数に欲しいくらい(笑)。
例えば範囲外のインデクスのとき、予め Fallback 的な値を決めておく等、とても簡潔に書ける。
●例:範囲内なら配列の値を返し、範囲外なら 0 を返す
int[] arr = {0,1,2,3,4};
int GetOrFallback(int idx) => arr.TryGetValue(idx, out var res) ? res : 0;
ところで少し話はずれるが、Dictionary で値の取得を試みるとき、どうやっているだろうか?
例えば以下のような感じに書けると思うが、① と ② では実行速度が結構違う。
① ContainsKey を用いてから取得
if (dic.ContainsKey(key)) //dic は Dictionary とする
{
var res = dic[key];
//ここで res を使った処理
}
② TryGetValue で取得
if (dic.TryGetValue(key, out var res)) //dic は Dictionary とする
{
//ここで res を使った処理
}
① と ② は同じ結果にはなるが、実は実行速度は ① の方が約2倍かかる(遅い)。
理由は簡単で、① は ContainsKey で1度 get, インデクサ(dic[key]) でもう1度 get の計2回 get (取得)するからである。TryGetValue は1回で済むので、その分速い。
ほんのちょっとしたことでも、実行速度を改善できたりするので、特に高速なメインループ処理を実現したい時などに思い出してみると良い(Unity みたいな、毎フレーム回す処理には、徹底すれば意外と効果がある=アルゴリズム変更しない限りでは「手数を減らす」のが直接速度改善に繋がる)。
(関連記事)
【Unity】【C#】配列・リストのシャッフル
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
【C#】配列やListなどの中身(要素)を見る拡張メソッド Dump
- 関連記事
-
-
【C#】二分探索の実装(範囲インデクス指定実装)
-
【C#】配列 や List の TryGetValue
-
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
-
【C#】 LowerBound, UpperBound (二分探索)
-
【C#】クラスのフィールド名を文字列の配列で取得する
-
category: C#
thread: プログラミング
janre: コンピュータ
tag: C#ライブラリ C#リファレンス 配列操作