4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2023-12-01

概要

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行を書き込み アクティビティで、以下の式を実行すると、出力結果から判別できます。

1行を書き込み
(なにか変数.GetType().IsValueType And Not(なにか変数.GetType().IsPrimitive Or なにか変数.GetType().IsEnum)).ToString()

→ 「なにか変数」が構造体の場合True、そうでなければFalseが出力されます。

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を代入
  2. 変数Bに、変数Aを代入
  3. 変数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を代入
  2. 変数Bに、変数Aが指し示す値複製して代入
  3. 変数Aに、200を代入

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

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

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

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

image.png

image.png

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

前提 データテーブル型のdt1、dt2の変数がある

  1. dt1に、3行のデータテーブルを作成して設定
  2. dt2に、dt1を代入
  3. dt1から1行削除(0行目を削除)
  4. dt2の行数を表示

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

image.png

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

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

これが参照型です。

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

もう1つ、新規ワークフローを作って、

  1. i1i2というInt32の変数を作成
  2. 代入i1に100を代入
  3. 代入i2i1を代入
  4. 代入i1に200を代入
  5. 1行を書き込みi2.ToString()を出力

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

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

2.参照 #とは

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

2.1 空っぽの参照

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

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

image.png

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

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が発生する話をしましたが、

  1. 代入dtNothingを代入
  2. 1行を書き込みで、dt.RowCount.ToString()を出力

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

ところで、もう1つの例。Int32型の変数での場合です。
Int32型の変数iNothingを代入して、次に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

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

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

3a. String型の変数sを設定し、初期値を設定せずに、1行を書き込みsを出力する
3b. String型の変数sを設定し、代入sNothingを代入した後、1行を書き込みsを出力する
3c. String型の変数sを設定し、代入sNothingを代入した後、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行のデータを設定する
  2. ワークフロー ファイルを呼び出し で、Subワークフローを呼び出す(引数にdtを渡す)
  3. 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行のデータを設定する
  2. ワークフロー ファイルを呼び出し で、Subワークフローを呼び出す(引数にdtを渡す)
  3. 1行を書き込みdt.RowCount.ToString()する
  • Subワークフロー 引数: in_dt 方向: 入力
  1. in_dt に、10行のデータを設定する
  2. データ行を削除 で、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 蛇足

(この記事を作成した事情とかが変わったので削除しました)

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?