概要
UiPath Studioで開発する人向けの、.NETの型の、やや深い部分の話。いわゆる「VB.NETの表記」とかです。
「変数とは何か」みたいな基本的な部分は、理解している前提で書きます。
この記事では、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行を書き込み
アクティビティで、以下の式を実行すると、出力結果から判別できます。
(なにか変数.GetType().IsValueType And Not(なにか変数.GetType().IsPrimitive Or なにか変数.GetType().IsEnum)).ToString()
→ 「なにか変数」が構造体の場合True、そうでなければFalseが出力されます。
(なにか変数.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がある
- 変数Aに、100を代入
- 変数Bに、変数Aを代入
- 変数Aに、200を代入
これが終わったタイミングで、変数Aと変数Bの値は何でしょうか?
直感的には、
- Aは、3番目の操作で200になっている
- Bは、1番目の操作でAに代入した100が代入されているので、100になっている
だと思います。
ところがAとBが参照型だと、両方とも「200」になります。
というのも、2番目の操作では、「変数Bには、変数Aの実際の値が格納されている場所」が格納されているからです。その値は、3番目の操作で「200」に書き換わっているので、変数Bの値を取得しても、200になってしまいます。
ちょっとこれ、分かりにくいですよね?
もちろん、その分かりにくさを克服するというか、
- 変数Aに、100を代入
- 変数Bに、変数Aが指し示す値を複製して代入
- 変数Aに、200を代入
という風に処理(実装)すれば、使い分けができるのですが。値型でよく使われるような型って、いちいち「複製する」というワンテンポを踏むのは、処理速度の面でも、実装(UiPathで言うところのワークフロー作成)の面でも、あまり宜しくないのです。
「参照型」の動作を実際に体験してみる
ちょっと概念的な話が続いたのですが、実際にUiPathでの「参照型」の変数の動きを確認してみましょう。
- UiPath Studioで新規プロセスを作ります。
Windows
かWindows - レガシ
あたりで。言語設定する場合はとりあえずVB.NET
にしましょう。 -
Main Sequence
のスコープに、DataTable型の変数を2つ設定します。単純なテストなのでdt1
とdt2
としましょう。 -
プログラミング
>データ テーブル
>データ テーブルを構築
アクティビティを配置します。 -
データ テーブルを構築
アクティビティで、3行のデータテーブルを設定し、出力先をdt1
とします。 -
代入
アクティビティを次に配置して、dt2
に、dt1
を代入します。 -
プログラミング
>データ テーブル
>データ行を削除
アクティビティを配置し、行インデックス
に0を、データ テーブル
にdt1
を設定します。 - 最後に
1行を書き込み
アクティビティを配置して、dt2.RowCount.ToString()
を設定します。
さて、このワークフローの処理内容を箇条書きにすると、
前提 データテーブル型のdt1、dt2の変数がある
- dt1に、3行のデータテーブルを作成して設定
- dt2に、dt1を代入
- dt1から1行削除(0行目を削除)
- dt2の行数を表示
さて、実行結果はどうなるでしょうか?
はいはーい。2が出ました。
上記の箇条書きで、2行目のタイミングでは、dt1には3行のデータがあって、dt2にもそれが代入された、というところまではシンプルな話です。
その次にdt1で1行消したのが、dt2にも影響している(dt2の行数も2行と判定される) っていうのが、めっちゃエモい直感に反した動作になっている、だったりしませんか?
これが参照型です。
説明は雑にするけど値型も試してみる
もう1つ、新規ワークフローを作って、
-
i1
とi2
というInt32の変数を作成 -
代入
でi1
に100を代入 -
代入
でi2
にi1
を代入 -
代入
でi1
に200を代入 -
1行を書き込み
でi2.ToString()
を出力
はい、100が出ました。わーわーパチパチ。上記のDataTableの例とは対照的に、i2
にi1
を代入したタイミングでのi1
の値(100)がi2
に保存されます。
その後のi1
に新たに200を代入したことに影響されず、値がそのまま表示されたわけです。
「変数は値を入れておく箱」という表現だと、割とこっちのほうがしっくり来る動作だと思いますけどね。
2.参照 #とは
なんとなくここまでで「参照型」のことをイメージできたと思います。
ここでは更に掘り下げます。混乱させます。
2.1 空っぽの参照
基本的には参照型の変数は、値を設定しないと、「何も参照されていない」状態になります。当たり前ですね。
めちゃくちゃシンプルな話ですが、新規ワークフローで、dt
というDataTable型の変数を作って、(当然「規定」欄は空白にして)、初手で1行を書き込み
で、dt.RowCount.ToString()
だけを設定し、実行してみてください。
めっちゃ叱られます。このエラー、見たことある人、かなり多いんじゃないでしょうか?
Object reference not set to an instance of an object.
要するに、Objectへの参照が設定されてないよってことです。上に書いたとおり「何も参照されてない」モノに対して「お前の行数、何行ですか?」と訊いたので、「参照ないですよ」と叱られたわけです。
System.NullreferenceException
例外の名前も、Null(空白の) Reference(参照) Exception(例外)と、ここまでの読み解けば、かなり状況を理解しやすい感じですよね?
ぬるぽ
ちょっと話題が逸れますが、ぬるぽ って見たことありませんか?あと、それに「ガッ」と返事してる人とか。
アレ、.NETで言うところの、上記のNullReferenceExceptionに相当するものが、Javaという言語(名前は聞いたことあると思います)では、NullPointerException(略して「ぬるぽ」)と表記されることに起因しています。
「ぬるぽ」には「ガッ」と殴ってるアスキーアート(イラスト)で返すのがお約束になってますが、これ、「初期値の設定し忘れて例外出した」という凡ミスに、「何してるん」とツッコミいれてる、って文脈です。
2.2 Nothing
VB.NETの記法では、少し変な言い回しになってしまいますが、(参照型の変数で)参照先が設定されていない変数 の値(≒参照先)を、Nothing
と記載します。
少し上で「変数を作っただけで何も設定していないDataTableの行数を取得する」でNullReferenceExceptionが発生する話をしましたが、
-
代入
でdt
にNothing
を代入 -
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で落ちるんです。
これが本稿の最初のほう(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ワークフロー
- DataTable型変数
dt
に、3行のデータを設定する -
ワークフロー ファイルを呼び出し
で、Subワークフロー
を呼び出す(引数にdt
を渡す) -
1行を書き込み
でdt.RowCount.ToString()
する
- Subワークフロー 引数:
in_dt
方向:入力
-
データ行を削除
で、in_dt
から1行削除する
「DataTable型は参照型」という情報を無視しての、直感的にはSubワークフロー
では、引数は入力
でしかないので、その中でDataTableに対して行った操作は、Mainワークフロー
の変数dt
には影響しないように見えます。
が、実際に動かすとわかるように、Subワークフロー
を呼び出した後、 その中身は1行減ってます。
3.2 参照を書き換えるパターン
ところで、参照型は参照先の場所が、変数という箱に入っている、という話をしました。
これはワークフロー ファイルを呼び出し
の引数で渡すときも同じです。
なので、3.1でのSubワークフロー
に、1つ追加して、
- Mainワークフロー
- DataTable型変数
dt
に、3行のデータを設定する -
ワークフロー ファイルを呼び出し
で、Subワークフロー
を呼び出す(引数にdt
を渡す) -
1行を書き込み
でdt.RowCount.ToString()
する
- Subワークフロー 引数:
in_dt
方向:入力
-
in_dt
に、10行のデータを設定する -
データ行を削除
で、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の公式ドキュメント にも書いてはあるんですが、めっちゃ言葉足らずなので、たぶん前提知識がかなり深くないと理解できないと思うんです。
4. まとめ
「値型」と「参照型」の変数をきちんと理解して、楽しいUiPathライフを送りましょう!
4.1 蛇足
(この記事を作成した事情とかが変わったので削除しました)