【UEFN】Verseのシャローコピーとディープコピー(Verseの学習⑭)

ディープコピーとシャロウコピー

Verseのコピー演算子で気を付けたいこと

3月末からVerseを触ってきましたが、8月になって初めてあることに気づきました。

それは、

Verseのmapやarrayはディープコピーされる!

ということです。 辞書型やリスト型のコピー演算子は、シャロウコピーとなっているプログラミング言語の方が一般的だと思われ、この5か月間、Verseもシャロウコピーだろうと完全に勘違いしてコーディングを行っていました。😢

どういった場合がシャロウコピーになり、どういった場合がディープコピーになるのか、EpicのTim御大が直々フォーラムで返答していました。

Question on Copying Variables - Verse - Epic Developer Community Forums

投稿者の方が

Do variables create shallow, or deep copies when assigning one to another?

変数はシャロウコピーですか?ディープコピーですか?

と聞いたのに対して

Tim御大自身が

Deep, stopping only at instances of classes, which are referenced.

ディープです。クラスのインスタンス時にそれは終了し、参照になります。

と答えています。

え、じゃあ、mapやarrayは?となったのですが、検証してみると、ディープでした。

な、な、なんと・・・・!

JavaPythonC#などでプログラムを書いていた人はむしろびっくりな結果です。

そもそもディープコピー、シャロウコピーとはなんなのか、という説明もしながら、これを今回検証していきたいと思います。

シャローコピーとディープコピーの検証

Verseに限らず、多くのプログラミング言語では変数を準備して、それに対して作業を行っていきます。

この変数の情報はメモリに記憶されていきますが、この変数の値を別の変数にコピーしたい場合が多々発生します。

多くのプログラムでは、代入演算子"="でこれを実行しているかと思います。

# ValueAの値をValueBにコピーする
ValueA := 10
ValueB := ValueA

このプログラムの場合、ValueAの10という値が、ValueBにもコピーされます。 メモリ上には以下のように別々に記憶されます。

AとB

次にコピーした後に、ValueAを変更してみます。

# ValueAの値をValueBにコピーする
var ValueA : int = 10
ValueB := ValueA
ValueA = 1

当然ですが、メモリ上は以下のようになります。

Aを変更後

これがディープコピーの動作になります。

mapのコピーを検証

では、これをmap型で実施してみます。 MapAを準備してキーを2つセットし、その後、MapBにMapAをコピー、MapAとMapBをプリントしています。

var MapA : [int]string = map{}
if:
    set MapA[1] = "One"
    set MapA[2] = "Two"

MapB := MapA
if:
    set MapA[1] = "Ichi"
    set MapA[2] = "One"

Print("This is MapA")
for(Key->Value : MapA):
    Print("{Key}->{Value}")

Print("This is MapB")
for(Key->Value : MapB):
    Print("{Key}->{Value}")
        return

Fortnite上で実行してみると以下のようにプリントされます。

実行結果

これは以下のようになったことを意味しています。

まず、最初にMapAの値が、すべてMapBにコピーされます。

MapAからMapBへのコピー

次にMapAの値を変えています。

MapAの値を変更

MapAとMapBでは完全に別々のメモリを使用していることがわかる結果となりました。

え?普通の結果では?と思われる方もいるかもしれません。ディープコピーは要素全体を丸っとコピーする、いわば普通のコピーとなるため、むしろプログラムを初めて書かれる方にはこの動作の方が理解しやすいかとは思います。

ただ、前述したように多くの言語では、辞書型や配列型をディープコピーせず、シャロウコピーする方が一般的なため、プログラムをある程度されてきた方はかなり注意が必要なポイントです。

では、次にシャロウコピーとなる場合に関して検証します。

インスタンスのコピー

辞書を管理するクラス、map_managerを作成し、辞書はそのなかで管理する形とします。

その上で、その辞書型のインスタンスを準備し(InstA)、別のインスタンスへコピーを実行してやります。

コピー後にもとのインスタンスの値を変え、コピー元とコピー先、双方がどう変わったかチェックします。

テストコードはこのような形です。

まず、map_managerクラスです。

map_manager := class:
    var IntToStringMap : [int]string = map{}

    AddValue<public>(Key : int, Value: string):void=
        if:
            set IntToStringMap[Key] = Value
    PrintMap<public>() : void=
        for(Key->Value : IntToStringMap):
            Print("{Key}->{Value}")
        return

これを使用して以下のようなコードでテストします。

InstA := map_manager{}
InstA.SetValue(1, "One")
InstA.SetValue(2, "Two")

InstB := InstA

InstA.SetValue(1, "Ichi")
InstA.SetValue(2, "Ni")

### print b
Print("This is InstA")
InstA.PrintMap()
Print("This is InstB")
InstB.PrintMap()

結果は次のようになりました。

インスタンスのコピー

InstAからInstBにコピーしてから、InstAの値を変更しているのに、InstBの値も変わっています。

これはなぜかというと、インスタンスの変数が情報として保持しているのは「私はここのメモリを参照します」というメモリの参照情報だけだからです。

そのため、その参照先の情報はInstAとInstBでまったく同じものとなっており、InstAの参照先を変えると、InstBも変更されることになります。

インスタンス変数が持っているのは参照情報だけ

こうした参照情報だけのコピーをシャロウコピーと呼んでいます。

Verseでもインスタンスのコピーはシャロウコピーになっていることが確認できました。

まとめ

今回、2つのことを紹介しています。

プログラマーの方向けにお伝えしているのは、

  • マップやリストもディープコピーされる

初心者プログラマーの方向けにお伝えしているのは、

ということになります。

オブジェクト指向で組んでいくと、どういった場合がディープで、どういった場合なのがシャロウなのか、把握して組んでいく必要があり、代入演算子の動作は理解しておいた方がいいかと思います。(自分はmapはシャロウだと5か月間違えていた😢)

今回の記事が皆様のお役に立てば幸いです。

UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作っています。

ringogames.hatenablog.com

ぜひほかの記事も見ていただけると嬉しいです。

RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。

Twitterはこちら

twitter.com