Data Lists
Data Listsは、Data Tokensをインデックスごとに格納するもので、C#のListと似た仕組みです。Data Listの関数のほとんどは、内部的なC#のリストをラップしたものであるため、より詳細な仕様を知りたい場合はC#のリストに関するドキュメントを参照してください。
Data Listsは、VRCJSONを使用してJSON文字列との間でシリアライズを行うことができます。これが現在推奨されている、ネットワーク経由でData Listsを同期する方法です。
UdonSharpを使用する場合は、using VRC.SDK3.Data;ディレクティブを含めることでData Listsが利用可能になります。
Properties
| プロパティ | Result |
|---|---|
Capacity | リストの容量(Capacity)を設定または取得します。詳細はC#のドキュメントを参照してください。 |
Count | リストに含まれる要素の数を取得します。 |
Functions
| Function | Input | Output | Result |
|---|---|---|---|
Add | DataToken | リストの末尾にトークンを追加します。 | |
AddRange | DataList | このData Listの末尾に、別のData Listの値をすべて追加します。 | |
BinarySearch | DataToken value | int index | バイナリサーチ(二分探索)アルゴリズムを使用して、比較によりList内の特定の要素を検索します。バイナリサーチを実行するには、リストがソートされている必要があります。バイナリサーチの詳細については、C#ドキュメントを参照してください。 |
BinarySearch | DataToken value, int startIndex, int count | int index | 指定された開始インデックスから、指定された数だけ要素を進んだ範囲内でバイナリサーチを実行します。この関数は比較に基づいて検索を行う点に注意してください。辞書やリストを比較する場合、内容ではなく要素数で比較されます。そのため、リストの中から特定の辞書やリストを検索する用途には適していません。 |
Clear | このリストからすべての値を削除します。 | ||
Contains | DataToken value | bool result | Data Listに指定された値が含まれている場合、trueを返します。 |
DeepClone | DataList result | DataListを、同じ値をすべて含む新しいDataListへと複製します。これはディープクローンを行うため、内部の各DataListやDataDictionaryの再帰的な走査を行い、その内容もコピーします。ただし、配列のような他の構造体は参照されず、それらは元のオブジェクトと同じ参照を保持したままとなります。 | |
GetRange | int index, int count | DataList result | DataListの一部を別のDataListにコピーします。indexやcountが範囲外の場合はfalseを返します。 |
IndexOf | DataToken item | int index | 指定したオブジェクトを検索し、DataList全体の中で最初に見つかった位置のインデックス(0から始まる)を返します。見つからない場合は-1を返します。 |
IndexOf | DataToken item, int startIndex | int index | 指定したオブジェクトを検索し、DataList内の指定されたインデックスから末尾までの範囲で最初に見つかった位置のインデックス(0から始まる)を返します。見つからない場合は-1を返します。 |
IndexOf | DataToken item, int startIndex, int count | DataToken output | DataList内の指定されたインデックスから始まり、指定された要素数を含む範囲内で、指定されたオブジェクトを検索し、最初に見つかった要素の0から始まるインデックスを返します。見つからなかった場合は-1を返します。 |
Insert | int index, DataToken input | bool success | リストの中間にトークンを挿入します。指定されたインデックス以降のすべてのエントリは、1つずつ後ろにずれます。インデックスが範囲外の場合はfalseを返します。 |
InsertRange | int index, DataList input | DataListの中間に別のDataListを挿入します。指定されたインデックス以降のすべてのエントリは、後ろにずれます。インデックスが範囲外の場合はfalseを返します。 | |
LastIndexOf | DataToken item | int index | DataList内で指定されたオブジェクトを検索し、最後に見つかった要素の0から始まるインデックスを返します。見つからなかった場合は-1を返します。 |
LastIndexOf | DataToken item, int startIndex | int index | DataListの最初の要素から指定されたインデックスまでの範囲内で指定されたオブジェクトを検索し、最後に見つかった要素の0から始まるインデックスを返します。見つからなかった場合は-1を返します。 |
LastIndexOf | DataToken item, int startIndex, int count | int index | 指定されたオブジェクトを検索し、DataList内の指定された要素数を含み、指定されたインデックスで終わる範囲内で、最後に出現する要素のゼロから始まるインデックスを返します。見つからない場合は -1 を返します。 |
Remove | DataToken value | bool success | 指定された値が最初に出現する要素を削除します。一致する値が見つかった場合は true を返し、見つからなかった場合は false を返します。 |
RemoveAll | DataToken value | bool success | 指定された値に一致するすべての要素を削除します。一致する値が1つでも見つかった場合は true を返し、見つからなかった場合は false を返します。 |
RemoveAt | int index | 指定されたインデックスにある要素を削除します。 | |
RemoveRange | int index, int count | リストから指定した範囲の要素を削除します。 | |
Reverse | リスト内のすべての要素の順序を反転させます。 | ||
Reverse | int index, int count | 指定されたインデックスから開始し、指定された数だけリストの末尾方向に進む範囲内の要素の順序を反転させます。 | |
SetValue | int index, DataToken input | 指定されたインデックスに DataToken を設定します。 | |
ShallowClone | DataList result | DataListを複製し、すべての値を含む新しいDataListを作成します。これはディープコピーではないため、DataListに他のData Containerへの参照が含まれている場合、それらの参照は元のままとなります。 | |
Sort | リスト内のすべての要素を並べ替えます。すべての要素が同じ型である場合、その型のネイティブな比較演算によって並べ替えが行われます。DataListに異なる型が含まれていても、それらすべてが数値である場合は、数値変換を行って並べ替えます。DataListに数値以外の異なる型が含まれている場合は、Null, Number, String, DataList, DataDictionary, Referenceの順序で並べ替えます。 | ||
Sort | int index, int count | Sortと同じ操作を行いますが、指定したindexから開始し、指定したcount分だけリストの末尾に向かって進んだ範囲内でのみ実行されます。 | |
ToArray | DataToken[] output | DataListをDataToken配列に変換します。 | |
TrimExcess | 要素数が閾値よりも少ない場合、容量をDataList内の実際の要素数に設定します。 | ||
TryGetValue | int index | DataToken output | 指定したインデックスからトークンを取得し、out DataTokenに格納します。成功した場合はtrueを返します。 |
TryGetValue | int index, TokenType expected | bool success, DataToken output | 指定したインデックスからトークンを取得し、out DataTokenに格納します。成功した場合はtrueを返します。このバージョンのTryGetValueにはTokenTypeが含まれており、自動的に型チェックが行われます。型が一致しない場合は、DataError.TypeMismatchとともにfalseを返します。 |
Jsonから生成されたData Listに対してContains、IndexOf、LastIndexOfのようにすべての値に影響を与える、または参照する関数を呼び出すと、まだ解析されていないすべてのトップレベルの値が解析されます。値が多数ある場合は負荷が高くなる可能性があるため注意してください。一度解析されると、以降の操作は低コストになります。
DataListから値を取得する
DataListから値を取得する方法はいくつかあります。それぞれ用途が異なるため、目的に合ったものを選んでください。
TryGetValue
リストから安全に値を取得したい場合は、TryGetValue を使用することが推奨されます。これは、値の取得に成功したかどうかで true または false を返す関数です。成功時と失敗時の挙動を明確にするため、if や branch の条件式の中で使用するようにしてください。
if (list.TryGetValue(0, out DataToken value))
{
Debug.Log($"Success! {value}");
}
else
{
Debug.Log("Failed! {value}");
}
失敗した場合、受け取る DataToken 自体は有効ですが、データではなくエラーが格納されます。
このメソッドは、特定の場所から値を取得したいものの、その値が正確に何であるかは問わない場合に適しています。
この関数には型チェックが組み込まれていないため、if、branch、switchなどを用いて何らかの型チェックと併用してください。特定の型のみを扱う場合は、自動的に型チェックを行ってくれるTokenType付きのTryGetValueを使用することをお勧めします。
TokenType付きのTryGetValue
リストから値を取得する際、その型が不明な場合は型チェックを行うことが重要です。自身でコードを記述して型チェックを行うことも可能ですが、煩雑になりがちです。代わりに、TokenTypeを含むTryGetValueを使用すれば、予期した型である場合のみ値を取得するよう指定できます。それ以外の場合はfalseが返されるため、適切に処理を行うことが可能です。
このメソッドは、特定の場所から特定の値を取得したいものの、データが外部ソース由来であるため、ソースが正しいデータを持っているか確信が持てない場合に有効です。
// You could do it this way, but it's a bit ugly
if (list.TryGetValue(0, out DataToken value)) {
if (value.TokenType == TokenType.DataDictionary)
{
Debug.Log($"Success! Matching dictionary has {value.DataDictionary.Count} items");
}
}
// This approach has a type check built in! It's functionally the same, but streamlined.
if (list.TryGetValue(0, TokenType.DataDictionary, out value)) {
Debug.Log($"Success! Matching dictionary has {value.DataDictionary.Count} items");
}
短縮版のブラケット構文
UdonSharpでは list[5] = "value"; のようなブラケット構文を使用してDataListの要素を設定・取得したり、Udon Graphでは DataList Get Item ノードを使用したりすることもできます。この方法は記述が簡潔で使いやすいですが、不正な操作を行うとUdonBehaviourが停止する可能性があるため、完全な安全性は保証されない点に注意してください。この方法は、データに対して完全な制御が可能であり、そのデータが存在し、かつ期待する型であることを保証できる場合にのみ使用してください。それ以外の場合は、何らかの形式で TryGetValue を使用することを推奨します。
list[0] = 5;
list[1] = 10;
// This makes the assumption that index 0 and 1 will always contain integers.
// This is a safe assumption to make since we set them just above in a controlled environment.
// If the data is coming from an external source, we shouldn't make these assumptions!
int sum = list[0].Int + list[1].Int;
DataListの初期化
UdonSharpでは、private変数内でDataListを初期化できます。これにより、コードが実行される前に定義された既存のデータセットを持つことができます。また、ネストされた辞書や、DataTokenがサポートするあらゆるものにも対応しています。この構文の使用例を以下に示します。
private DataList _groceries = new DataList()
{
"Bananas",
"Grapes",
"Milk",
"Soda",
"Turkey",
"Ham",
"Roast Beef"
}
現時点では、UdonSharpは関数内でのこの形式の初期化をサポートしていません。これはUdonSharpに対する機能リクエストとなります。
現在、UnityはDataListのシリアライズに対応していないため、シリアライズされるパブリック変数への使用は推奨されません。private変数または[NonSerialized]属性を付与したパブリック変数に対してのみ使用してください。これは現在開発中の機能に対する追加要素となります。
ネットワーク経由で他のプレイヤーとData Listを同期する
Data Listを直接同期することはできません。しかし、VRCJsonを使用してJSON文字列へのシリアライズ/デシリアライズを行うことは可能です。これが現在推奨されている、UdonSyncでData Listを同期する方法です。
これを行う方法の1つとして、OnPreSerializationとOnDeserializationを使用してJSON文字列をシリアライズおよびデシリアライズする方法があります。この手法を使えば、コードの他の部分でシリアライズについて悩む必要はなく、単に値を設定するだけで済みます。
[UdonSynced]
private string _json;
private DataList _list;
public override void OnPreSerialization()
{
if (VRCJson.TrySerializeToJson(_list, JsonExportType.Minify, out DataToken result))
{
_json = result.String;
}
else
{
Debug.LogError(result.ToString());
}
}
public override void OnDeserialization()
{
if(VRCJson.TryDeserializeFromJson(_json, out DataToken result))
{
_list = result.DataList;
}
else
{
Debug.LogError(result.ToString());
}
}
FAQ
なぜ各型用のToArrayを用意しないのか?
各データ型に対して ToArray メソッドを用意するのが理想的ですが、現時点では Udon がジェネリックをサポートしていないため、実装は不可能です。ToStringArray、ToFloatArray、ToDoubleArray といった個別のメソッドを作成することは技術的に可能ですが、考えられるすべての型を網羅しようとすると、メソッドが肥大化してしまいます。さらに、Udon 2 でジェネリックがサポートされれば、こうしたメソッドは非推奨となってしまいます。また、基本的な ToArray メソッドだけでは、大きなメリットは得られません。真の利点は ToArray(typeof(Collider)) のように、object 型に対して ToArray を実行し、キャストの必要性を排除できる点にありますが、あらゆる object に対して ToArray をサポートするのは現実的ではなく、特に object 型に対する ToArray のサポートは、トークンを扱うよりもさらに効率が悪い結果となります。
DataToken から値を取得する作業はいくぶん煩雑になりがちですが、DataToken はまさにその目的のために設計されており、この作業を補助するいくつかのユーティリティが用意されています。
配列も似たようなものですが、どのような違いがあるのでしょうか?
配列(Arrays)は、多くの値を順番に格納し、インデックスでアクセスするために使用される同様の構造です。配列は非常にシンプルで、まさにその目的だけに特化しており、極めて効率的です。DataListはより複雑な型であり、はるかに多くのことができます。例えば、配列は最初に作成する際に特定の長さを指定して初期化する必要があり、新しい配列を作成して置き換えない限り、項目を追加することはできません。しかし、あなどってはいけません。リストではなく配列を使用するべき妥当な理由は依然として存在します。
DataListではなく配列を使用すべきなのはどのような時ですか?
配列よりもDataListを選択するのは、特定の機能が必要な場合に行うべきであり、すべてを置き換える必要はありません。
- コンテナに対して動的に項目を追加したり削除したりしたい場合。配列ではこれができません。
- 1つのコンテナに複数の異なる型を同時に含めたい場合。配列ではこれができません。
- コンテナの中にコンテナを任意に含めたい場合。配列でも可能ですが、厳密な深さを定義する必要があります。DataListは、必要に応じて任意に深くネストすることができます。
DataListではなく配列を使用すべきなのはどのような時ですか?
- パフォーマンスが重要な場合(毎フレームコンテナを反復処理する場合など)。DataListはトークンから値を取り出す際に、ごくわずかなパフォーマンスオーバーヘッドが発生する可能性があります。
- コンテナをネットワーク経由で同期したい場合。DataListがどうしても必要な理由があるなら、技術的にはJSONを通じてこれを行うことができますが、通常の配列同期と比較してパフォーマンスと帯域幅の両面で非常に大きなコストがかかります。
- コンテナに特定の型のみを格納すればよい場合。DataListでも当然可能ですが、C#の厳密な型定義という特性を回避してしまいます。つまり、コードエディタがコンテナに含まれる型を正確に把握できなくなり、本来ならコンパイルエラーになるようなバグを書いてしまう可能性があります。
- Data Tokensが直接サポートしていない型を格納したい場合。Data Tokensはオブジェクト参照とボックス化を利用することであらゆる型を格納できますが、理想的とは言えません。参照を取り出したうえで、目的の型にキャストする必要があります。
最終更新: