[UiPathユーザー向け] .NETの型の話

# 概要
UiPath Studioで開発する人向けの、.NETの型の、やや深い部分の話。いわゆる「VB.NETの表記」とかです。
「変数とは何か」みたいな基本的な部分は、理解している前提で書きます。

:::note warn
この記事では、UiPath Studioで開発・保守する上で必要な部分に絞って、場合によっては意図的に（話を簡単にするために）端折っている要素があります。
.NETでのプログラミングを本格的に学ぶ場合、この記事では説明されていない要素・動作を加味する必要が出てくる場合がありますので、あらかじめご了承ください。
:::

# 1. 値型と参照型の話
UiPath Studioでワークフローを組む上では、いわゆる「変数の型」は避けて通れない要素です。
基礎的な部分で言えば、たとえば「String型は文字列を入れる」とか「Int32は整数を入れる」みたいに理解されていることが多いと思います。

さて、この「型」の種類なのですが、便宜上というか、動作として、「値型」と「参照型」という、2つのタイプにわけられます。

## 1.1 値型

ざっくりと、以下の形式が、.NETの仕様の（厳密にはC#の仕様に引きずられた）「値型」になります。

- SByte (System.SByte)
- Byte (System.Byte)
- Int16 (System.Int16)
- Int32 (System.Int32)
- Int64 (System.Int64)
- UInt16 (System.UInt16)
- UInt32 (System.UInt32)
- UInt64 (System.UInt64)
- Decimal (System.Decimal)
- Char (System.Char)
- Single (System.Single)
- Double (System.Double)
- 構造体
- 列挙型

UiPath Studioで比較的、よく見かけるのは、Int32とかDecimalでしょうか。
構造体とか列挙型については下記。

### 構造体
あらかじめ定義された、複数のデータを持てる型の一種です。
と書いてもピンと来ないと思います。実際のところUiPathで使うことってあまりないのです。
しいて言えば、日付・時間を記録する ``System.DateTime`` なんかがそれです。

### 列挙型

これも構造体と同じで、あまり使うことはないのです。DateTime以上に直接的に見かけることはない気がします。

### 構造体や列挙型を見極める

あまり良い方法がない（少なくとも2023/12/02現在、UiPath Studioで、ワークフロー作成中に区別する方法はない）のですが、`` 任意の名前の変数 ``をUiPath Studioで定義して、値が入るようにした上で、おなじみの`` 1行を書き込み `` アクティビティで、以下の式を実行すると、出力結果から判別できます。

```VB.NET:1行を書き込み
(なにか変数.GetType().IsValueType And Not(なにか変数.GetType().IsPrimitive Or なにか変数.GetType().IsEnum)).ToString()
```
→ 「なにか変数」が構造体の場合True、そうでなければFalseが出力されます。

```VB.NET:1行を書き込み
(なにか変数.GetType().IsEnum).ToString()
```
→ 「なにか変数」が列挙型の場合True、そうでなければFalseが出力されます。

## 1.2 参照型

単純な引き算で、値型でなければ参照型です。 **基本的には。**

## 1.3 例外があります
### 面倒なヤツ、String型

上で「基本的には」と書いたのが、String型という~~クソ~~面倒なのが居るからです。
細かい解説をしてると話が逸れるというか、拗れるので、ここでは以下のように覚えてください。

**厳密にはString型(System.String)は参照型だけど、UiPathで扱う上では、値型と思って問題ない（String型は値型として振る舞う）**

はい、これ大事なので10回ぐらい復唱してください。復唱しましたね？じゃあ次に行きましょう。

### もう1つ面倒なヤツ、GenericValue型

GenericValue型(UiPath.Core.GenericValue)というのも、実はStringと同じで、**厳密には参照型だけど、UiPathで扱う上では値型と思って問題ないヤツ**です。


## 1.4 そもそも「値型」と「参照型」の違いとか、区別する意味とか

ちょっと話が前後し気味なのですが、『 *いや「値型」「参照型」って何よ* 』っていう疑問が、ここまで来て湧いていると思います。はい、きわめて全うな疑問です。
ここで、「値型」と「参照型」の違いについて説明します。

割とありがちな説明で、「変数は値をいれておく箱です」というのが、UiPathの解説等では聞くことが多いと思います。これは一般的には間違いでないのですが、ちょっと語弊がある場合があります。

変数の型が「値型」に分類される型の場合、基本的に「箱です」という説明で問題ありません。
問題は「参照型」の場合で、文字通り、 **「実際のデータが入っている場所」** が、変数という箱の中に格納されています。
つまり、実際に取り出して使う値にたどり着くには、内部的に1ステップ余分に動作していることになります。（といっても、これは.NETが勝手にやってくれることなので、UiPath Studioでワークフローを作るとき、値を取り出したり、代入するだけなら、表面的には意識する必要はありません）

ではなぜ、そんな2つの型が存在するのか、という点について説明します。

### 架空のケースですが

さて、やや概念的になってしまうのですが。たとえば、以下のようなアクティビティというか、動作を想像してみてください。

> インターネットから何かデータをダウンロードして、変数に格納していく操作

UiPathだと基本的に「アクティビティ」という単位で動作して、上記のようなケースでは「ダウンロードが終わってから次に進む」ので、ちょっとイメージしにくいかもしれませんが。
パソコンの内部では、実際にはダウンロードしながらでも、他の操作ができます。
ブラウザで、何かプログラム（UiPath Studioのインストーラーとか）をダウンロードしながら、ほかの作業をした、なんて経験は皆さんあると思います。

そういう環境で、上記のような動作をするとき。変数に値をリアルタイムで書き込んでいきます。ここでは仮に、「ダウンロードしたデータ」という変数に、値を書き込んでいくとしましょう。
ところが、ダウンロードしている途中で、たとえばUiPathの``代入``のような操作で、
```
入手データ = ダウンロードしたデータ
```
という処理が行われたらどうなるでしょうか。

「ダウンロードしている途中のデータ」が**値型**（そのまま値を格納する）の場合、上記の操作が実行された瞬間の、ダウンロード途中のデータが``入手データ``に入ってしまうので、中身は尻切れトンボになって、あまり嬉しくありません。

「ダウンロードしている途中のデータ」が**参照型**の場合、変数``入手データ``の指す先は、``入手データ``に改めて別の値を代入するまでは、``ダウンロードしたデータ``と同じ場所になります。なので、最終的にダウンロードが終わったタイミングで参照すれば、きちんとダウンロードした値が取得できます。

こんな感じで、裏で他の処理が参照したり、あるいはデータそのものを複製することに不合理が生じるようなケースが想定されるような型は、基本的に参照型として作られている、と理解すると、なんとなくイメージしやすいのではないでしょうか。

### 値型の存在意義は？

では逆に「全部、参照型に統一すれば、こんな混乱は起きないのでは？」と考えるかもしれません。
ですが、それは幾つかの面で面倒なのです。

たとえば、以下のような順番で、操作を行うことを想定してください。

> 前提：数値が入る変数AとBがある
> 1. 変数Aに、100を代入
> 1. 変数Bに、変数Aを代入
> 1. 変数Aに、200を代入

これが終わったタイミングで、変数Aと変数Bの値は何でしょうか？

直感的には、

- Aは、3番目の操作で200になっている
- Bは、1番目の操作でAに代入した100が代入されているので、100になっている

だと思います。
ところがAとBが参照型だと、両方とも「200」になります。
というのも、2番目の操作では、「変数Bには、変数Aの実際の値が格納されている場所」が格納されているからです。その値は、3番目の操作で「200」に書き換わっているので、変数Bの値を取得しても、200になってしまいます。

ちょっとこれ、分かりにくいですよね？

もちろん、その分かりにくさを克服するというか、

> 1. 変数Aに、100を代入
> 1. 変数Bに、変数A**が指し示す値**を**複製して**代入
> 1. 変数Aに、200を代入

という風に処理（実装）すれば、使い分けができるのですが。値型でよく使われるような型って、いちいち「複製する」というワンテンポを踏むのは、処理速度の面でも、実装（UiPathで言うところのワークフロー作成）の面でも、あまり宜しくないのです。


### 「参照型」の動作を実際に体験してみる

ちょっと概念的な話が続いたのですが、実際にUiPathでの「参照型」の変数の動きを確認してみましょう。

1. UiPath Studioで新規プロセスを作ります。``Windows``か``Windows - レガシ``あたりで。言語設定する場合はとりあえず``VB.NET``にしましょう。
1. ``Main Sequence``のスコープに、DataTable型の変数を2つ設定します。単純なテストなので``dt1``と``dt2``としましょう。
1. ``プログラミング`` > ``データ テーブル`` > ``データ テーブルを構築`` アクティビティを配置します。
1. ``データ テーブルを構築`` アクティビティで、3行のデータテーブルを設定し、出力先を``dt1``とします。
1. ``代入`` アクティビティを次に配置して、``dt2``に、``dt1``を代入します。
1. ``プログラミング`` > ``データ テーブル`` > ``データ行を削除`` アクティビティを配置し、``行インデックス``に0を、``データ テーブル``に``dt1``を設定します。
1. 最後に``1行を書き込み``アクティビティを配置して、``dt2.RowCount.ToString()`` を設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703165/c9b82101-1f3c-8fae-62b8-22ee07c9bcd1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703165/aa358131-08d2-88ef-ad1c-ea67d9963243.png)

さて、このワークフローの処理内容を箇条書きにすると、

> 前提 データテーブル型のdt1、dt2の変数がある
> 1. dt1に、3行のデータテーブルを作成して設定
> 1. dt2に、dt1を代入
> 1. dt1から1行削除（0行目を削除)
> 1. dt2の行数を表示

さて、実行結果はどうなるでしょうか？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703165/1ec8743c-6643-aa3a-5172-47c1fbc60b7e.png)

はいはーい。2が出ました。

上記の箇条書きで、2行目のタイミングでは、dt1には3行のデータがあって、dt2にもそれが代入された、というところまではシンプルな話です。
**その次にdt1で1行消したのが、dt2にも影響している(dt2の行数も2行と判定される)** っていうのが、めっちゃ~~エモい~~直感に反した動作になっている、だったりしませんか？

**これが参照型**です。

### 説明は雑にするけど値型も試してみる

もう1つ、新規ワークフローを作って、
1. ``i1``と``i2``というInt32の変数を作成
1. ``代入``で``i1``に100を代入
1. ``代入``で``i2``に``i1``を代入
1. ``代入``で``i1``に200を代入
1. ``1行を書き込み``で``i2.ToString()``を出力

はい、100が出ました。わーわーパチパチ。上記のDataTableの例とは対照的に、``i2``に``i1``を代入したタイミングでの``i1``の値(100)が``i2``に保存されます。
その後の``i1``に新たに200を代入したことに影響されず、値がそのまま表示されたわけです。

「変数は値を入れておく箱」という表現だと、割とこっちのほうがしっくり来る動作だと思いますけどね。

# 2.参照 #とは

なんとなくここまでで「参照型」のことをイメージできたと思います。
ここでは更に掘り下げます。混乱させます。

## 2.1 空っぽの参照

**基本的には**参照型の変数は、値を設定しないと、「何も参照されていない」状態になります。当たり前ですね。

めちゃくちゃシンプルな話ですが、新規ワークフローで、``dt``というDataTable型の変数を作って、（当然「規定」欄は空白にして）、初手で``1行を書き込み``で、``dt.RowCount.ToString()``だけを設定し、実行してみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703165/e0090a04-c7f5-d730-a9c7-669ba7f93135.png)

めっちゃ叱られます。このエラー、見たことある人、かなり多いんじゃないでしょうか？

> Object reference not set to an instance of an object.

要するに、Objectへの参照が設定されてないよってことです。上に書いたとおり「何も参照されてない」モノに対して「お前の行数、何行ですか？」と訊いたので、「参照ないですよ」と叱られたわけです。

> System.**NullreferenceException**

例外の名前も、Null(空白の) Reference(参照) Exception(例外)と、ここまでの読み解けば、かなり状況を理解しやすい感じですよね？

### ぬるぽ
ちょっと話題が逸れますが、[ぬるぽ](https://ja.wikipedia.org/wiki/%E3%81%AC%E3%82%8B%E3%81%BD) って見たことありませんか？あと、それに「ガッ」と返事してる人とか。

アレ、.NETで言うところの、上記のNullReferenceExceptionに相当するものが、Javaという言語(名前は聞いたことあると思います）では、NullPointerException（略して「ぬるぽ」）と表記されることに起因しています。

「ぬるぽ」には「ガッ」と殴ってるアスキーアート（イラスト）で返すのがお約束になってますが、これ、「初期値の設定し忘れて例外出した」という凡ミスに、「何してるん」とツッコミいれてる、って文脈です。

## 2.2 Nothing

VB.NETの記法では、少し変な言い回しになってしまいますが、**（参照型の変数で）参照先が設定されていない変数** の値（≒参照先）を、``Nothing``と記載します。

少し上で「変数を作っただけで何も設定していないDataTableの行数を取得する」でNullReferenceExceptionが発生する話をしましたが、

1. ``代入``で``dt``に``Nothing``を代入
1. ``1行を書き込み``で、``dt.RowCount.ToString()``を出力

これでも同じエラーが起きます。
「初期化されていない（参照がない）``dt``に``参照先がない状態``を代入した」のですから、結局、参照先がないまま、行数を取得しようとしてコケる、という。当たり前のことが起きています。

ところで、もう1つの例。Int32型の変数での場合です。
Int32型の変数``i``に``Nothing``を代入して、次に``i.ToString()``を``1行を書き込み``すると、NullReferenceExceptionにはならずに、
> 0

が出力されます。これは「値型は、値が入っていない状態であれば、0のような所謂デフォルト値になる」というモノだと思ってください。

**これって言いかえれば、UiPath Studioで変数を定義し、「規定」に値を設定しない状態のモノは、``Nothing``を代入した状態になっている**ってことなんです。
``Int32``であれば0だし、DataTableであれば（何かしようとすると）NullReferenceExceptionが起きる。

変数の初期化とかについて、ここで結構、今までUiPathを使っていて漠然と経験・理解していたことが、繋がった感じじゃないでしょうか？

## 2.3 暗黙の初期値とNothingの境界

ところで、.NETには、変数の型を取得する関数があります。

```
変数名.GetType()
```

これで型が取れます。ただ型は型（System.Type型）で、文字列ではないので、型の名前を表示するには、

```
変数名.GetType().Name
```

とする必要があります。

これ自体はまあ、豆知識のようなモノです。そもそも変数の型を取得する必要性が、UiPathを使っている限り、まずないでしょう。「変数」パネル見れば型なんてわかりますもんね。

ただ、これを使うと、ちょーっと~~難解な~~面白い現象を見ることができます。

1\.Int32型の変数``i``を設定し、初期値を設定せずに、``1行を書き込み``で``i.GetType().Name``を出力する
2\. DataTable型の変数``dt``を設定し、初期値を設定せずに、``1行を書き込み``で``dt.GetType().Name``を出力する

はい。ここまでは（話の流れ的に）なんとなく予測できるかもしれませんが、出力内容は、1.が``Int32``と出力され、2.はNullReferenceExceptionで落ちます。

ここまでは良いんですよ。問題は次です。

3\. String型の変数``s``を設定し、初期値を設定せずに、``1行を書き込み``で``s.GetType().Name``を出力する

 **NullReferenceExceptionで落ちるんです。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703165/fa9831fc-b227-7ec8-bfc0-c1929a5224e5.png)

これが本稿の最初のほう（1.3）で、 **String型は参照型だけど、UiPathで扱う上では、値型と思って問題ない（String型は値型として振る舞う）** と書いた所以です。
一般的な動作ではあまり影響しないのですが、こういう、きわめて細かい部分では参照型として動作するのです。

ちなみに、上記のバリエーション実験として、

3a. String型の変数``s``を設定し、初期値を設定せずに、``1行を書き込み``で``s``を出力する
3b. String型の変数``s``を設定し、``代入``で``s``に``Nothing``を代入した後、``1行を書き込み``で``s``を出力する
3c. String型の変数``s``を設定し、``代入``で``s``に``Nothing``を代入した後、``s.GetType().Name``を出力する

3a.と3b. は空白行が出力されます。つまり「参照がない状態」でも、初期値の``""``（何もない文字列）が入っているように振る舞います。ですが3c.ではNullReferenceExceptionで落ちます。

ここまでStringの話をしましたが、GenericValueでも全く同じ結果になります。こいつらわかりにくいですね。

## 2.4 （余談）``Nothing``の判定

ちなみに。``Nothing``は「参照がない状態」です。
ただし、これは一種の抽象的なモノなので（この表現もめっちゃ抽象的ですが）、直接比較してはいけません。

つまり、``条件分岐 (If)`` アクティビティの式とかに、

```
なんか変数 = Nothing
```

みたいに条件式を設定してはいけません。まー、UiPath Studioが警告出してくれるので、すぐわかると思いますが。

上記のような判定をしたいときは、

```
IsNothing(なんか変数)
```

にすれば動きます。これを使うと、初期化されていない参照型の変数に、意図せずアクセスしてしまうことが防げます。

ここまでの話で推察できるかもしれませんが、``IsNothing(値型の変数)``は、常に``False``になります。値型の変数には「参照がない」状態が存在しないからです。
``IsNothing(初期化してないString型変数)``は``True``デスヨ。わかりますよね？

# 3. で、ここまでの話って何に影響するの？

はい。大変にややこしい説明をしましたが、ここまでの話がUiPathを使っていて、「想定外の動作」として直撃するパターンがあります。

``ワークフロー`` > ``呼び出し`` > ``ワークフロー ファイルを呼び出し`` で、引数として受け渡しする場合です。

## 3.1 シンプルなパターン

前述のようなStringやGenericValueのような例外を除いて、**参照型の変数を**引数で渡すとき、``入力``だけにしていても、呼び出し先のワークフローで、渡された引数に対して処理をすると、呼び出し元の値にも影響します。

具体例で言うなら、先ほどの参照型の動作を試したパターンの応用で、

- Mainワークフロー

1. DataTable型変数 ``dt`` に、3行のデータを設定する
1. ``ワークフロー ファイルを呼び出し`` で、``Subワークフロー``を呼び出す（引数に``dt``を渡す）
1. ``1行を書き込み``で``dt.RowCount.ToString()``する

- Subワークフロー 引数: ``in_dt`` 方向: ``入力``
1. ``データ行を削除`` で、``in_dt``から1行削除する

「DataTable型は参照型」という情報を無視しての、直感的には``Subワークフロー``では、引数は``入力``でしかないので、その中でDataTableに対して行った操作は、``Mainワークフロー``の変数``dt``には影響しないように見えます。
が、実際に動かすとわかるように、``Subワークフロー``を呼び出した後、 **その中身は1行減ってます。**

## 3.2 参照を書き換えるパターン

ところで、参照型は参照先の場所が、変数という箱に入っている、という話をしました。
これは``ワークフロー ファイルを呼び出し``の引数で渡すときも同じです。

なので、3.1での``Subワークフロー``に、1つ追加して、

- Mainワークフロー

1. DataTable型変数 ``dt`` に、3行のデータを設定する
1. ``ワークフロー ファイルを呼び出し`` で、``Subワークフロー``を呼び出す（引数に``dt``を渡す）
1. ``1行を書き込み``で``dt.RowCount.ToString()``する

- Subワークフロー 引数: ``in_dt`` 方向: ``入力``
1. ``in_dt`` に、10行のデータを設定する
1. ``データ行を削除`` で、``in_dt``から1行削除する

これを実行するとどうなるか、というと。``Subワークフロー``が呼び出された時点では、引数``in_dt``には、``Mainワークフロー``の``dt``の参照先が入っています。
ですが、``Subワークフロー``の最初の処理で、``in_dt``の参照先は、新規で作成された``10行のデータ``のDataTableに切り替わります。ここで注意すべきは、 **あくまで``Subワークフロー``の``in_dt``の参照先が変わっただけ** で、``Mainワークフロー``の``dt``の参照先には影響しないことです。

結果として、``Subワークフロー``の2行目で、10行の新しいDataTableから1行消されます **が、その結果は（方向:``入力``なので）書き戻されることはなく、``Mainワークフロー``の3行目の処理では、何も変更されていない``dt``の行数、すなわち3が表示されます。**

もちろんこのパターンで、``Subワークフロー``の引数``in_dt``の方向を``入力/出力``にすれば、10行のDataTableから1行が消えた9行のDataTable（への参照）が書き戻されて、``Mainワークフロー``の3行目の``1行を書き込み``では9が出力されます。

## 3.3 ``ワークフロー ファイルを呼び出し`` で例外が発生した場合

``ワークフロー ファイルを呼び出し`` の、呼び出し先で、``トライ キャッチ``で処理されない例外が発生した場合や、``再スロー``が行われた場合、引数の方向が``出力``や``入力/出力``であっても、**変数の書き戻しは行われません。**

すなわち、
- 値型の変数は``ワークフロー ファイルを呼び出し``の実行前の状態
- 参照型の変数は、参照先が``ワークフロー ファイルを呼び出し``の中で行われた処理は適用されるけど、書き戻しはされない

という違いが発生します。**これ意識してないと、思わぬ誤動作を引き起こす可能性があります。**

## 3.4 ここまでの話の例外

なんか例外的なモノばかり書いてますが、``ワークフロー ファイルを呼び出し``の実行時に、``分離``プロパティが有効（チェックボックスが入ってるとか``True``とか）だと、**すべての変数を、強制的に値型のように振る舞わせる**結果になります。つまり、3.1～3.3で記載したような、「呼び出し先のワークフローで、参照型の変数に干渉した結果、呼び出し元で想定外の書き換わりが起きる」ことは、まず発生しなくなります。

これは``分離``の処理は、プロセス（Windowsの実行単位）そのものを分割するため、プロセス間では変数を共有できないことに起因します。

UiPath使用時に限らない話をなのですが、Windowsの設計思想として、ほかのプロセスの変数（≒メモリ）の中身を見れてしまうのは、セキュリティ上、大変危険だからです。だって**ブラウザに入力してる途中のパスワードが、他のアプリから読み取れちゃうとか、絶対、嫌ですもんね**？

実のところ、ここら辺まで書いた説明って、[UiPathの公式ドキュメント](https://docs.uipath.com/ja/activities/other/latest/workflow/invoke-workflow-file) にも書いてはあるんですが、めっちゃ言葉足らずなので、たぶん前提知識がかなり深くないと理解できないと思うんです。


# 4. まとめ

「値型」と「参照型」の変数をきちんと理解して、楽しいUiPathライフを送りましょう！


## 4.1 蛇足
（この記事を作成した事情とかが変わったので削除しました）