🗑️

deferを使って後始末を | Verse, UEFN

2024/09/02に公開

前書き

突然ですが、Verseにおける「defer」という機能は使っていますか?存在自体は知ってるけど…あまり使ったことが無いな…そんな人が多いはず。

それもそのはずで、大抵の場合この機能を使わなくても成り立ってしまうことがあるから。なので、この記事を見て使おう!という気にならなくても、問題は無いといえるでしょう。

ですが、Verse言語のマイナー機能をあえて使うことで、コード全体の冗長性を無くしたり、予想外のバグに遭遇しづらくなる…というメリットがあります。ということで、さっそく例とともに使い方を見ていきましょう。

基本情報

そもそもdeferとは何なのか、知らない人向けに説明します。

こちらは非常に簡単で、(特に非同期処理で)deferが呼び出されたコードブロック内の処理が完了/キャンセルされた際にdefer内の処理が実行される。というものです。

完了というのは、SleepやAwaitの待機を終えたのち、コードブロック内の一番下の処理を実行し終わったことを指します。

キャンセルというのは、特にraceなどで外部から実行が中断された際を指します。

defer:
    function()
    functionB()
    functionc()

書き方は上記のように、defer内に非同期処理ではない関数や処理を入れます。

実装例

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }

sample_device := class(creative_device):

    @editable
    PropAsset : creative_prop_asset = DefaultCreativePropAsset

    @editable
    InitialPosition : transform = transform{}

    OnBegin<override>()<suspends>:void=
        race:
            SpawnAndMoves()
            Sleep(60.0)

    SpawnAndMoves()<suspends>:void=
        MaybeProp := SpawnProp(PropAsset, InitialPosition)(0)
        if(Prop := MaybeProp?):
            defer:
                Prop.Dispose()
            loop:
                NewTransform := transform:
                    Translation := InitialPosition.Translation + vector3{Z := 500.0}
                    Rotation := InitialPosition.Rotation
                    Scale := InitialPosition.Scale

                # Z + 500.0の位置に5秒かけて移動
                Prop.MoveTo(NewTransform, 5.0)

                # 初期位置に戻る
                Prop.MoveTo(InitialPosition, 5.0)

初期位置を決め、5秒かけて500cm上に上がり、5秒かけて元の位置(500cm下に下がる)という処理のコードです。deferがどこで使用されているかというと、SpawnAndMovesの中でSpawnPropが成功した際の処理のあたりです。

さて、このコードが実行されるとどのような結果になるでしょうか。Prop.Dispose()は小道具をデスポーンさせる関数だと思っていただいて。


コード全体の流れを見ていきましょう

SpawnAndMoves

  1. SpawnPropで小道具をスポーンさせる
  2. 小道具が正常にスポーンできた際にPropの中にスポーンした小道具が入る
    • この際、if内の処理が正常に実行される
  3. deferでスポーンした小道具に対しDispose(破壊処理)を行う
  4. loopで小道具を上下に動かす

OnBegin

  1. raceでSpawnAndMovesとSleep(60秒後に処理を完了する)を入れる
  2. 同時並行で動くためSpawnAndMoves内の処理が実行される。
  3. 60秒後にSpawnAndMovesの処理がキャンセルされる

というものです。

ここでdeferを使った理由はおわかりいただけたでしょうか。raceというのは呼び出した関数を強制キャンセルさせる機能です。そのため、例えloopが終わるような実装にし、その後にDisposeとしても、loopが終わる前にキャンセルさせるなんてこともあります。

ここで起こる問題点は、意味もない小道具がレベル上に残り続けてしまうという点です。だって、Dispose()を実行していないのだから。

つまり、deferを使う理由の一つとして、関数内やコードブロック内で生み出したものを正常に破棄するために使用する。というものになります。

ちなみにこのコードを実行すると、60秒間上下し、キャンセルされ次第deferが呼び出され、小道具が破棄されます。

ちなみにdeferを使わずに実装するとしたらどうしますか?実はdeferを使わなくても、スポーンと移動の処理を分けてraceの下にDisposeを追加するなど代替案はいくらでもあります。

    SpawnAndMoves()<suspends>:void=
        MaybeProp := SpawnProp(PropAsset, InitialPosition)(0)
        if(Prop := MaybeProp?):
            race:
                Moves(Prop)
                Sleep(60.0)
            Prop.Dispose()

    Moves(Prop:creative_prop)<suspends>:void=
        loop:
            NewTransform := transform:
                Translation := InitialPosition.Translation + vector3{Z := 500.0}
                Rotation := InitialPosition.Rotation
                Scale := InitialPosition.Scale

            # Z + 500.0の位置に5秒かけて移動
            Prop.MoveTo(NewTransform, 5.0)

            # 初期位置に戻る
            Prop.MoveTo(InitialPosition, 5.0)

しかし、場合によっては冗長なコードになってしまったり、そのDisposeを呼び出す関数自体も別のraceでキャンセルされる可能性もゼロではありません。

なので、手段はいくらでもあるためあえて使う必要もありません。しかし、あえて使うことで「バグの起こりにくい仕組み」が作れるともいえるでしょう。

コードブロック

余談です。この記事全体で「関数が終了した際に」ではなく「コードブロックが終了した際に」という表現を使ってきました。これにはちゃんと意味があり、defer自体は自身が呼び出されたスコープ内の処理が終わった際に処理が実行されるというものです。なので、if内で呼び出せばif内の全ての処理が終わった瞬間に呼び出されます。決して、常に関数が終わると同時ではないというのに注意が必要です。

公式ドキュメントも

なお、今回はdeferの基本部分にだけ触れましたが、公式ドキュメントにはもう少しだけ詳細なことを書いてたりもします。ここでは紹介しませんが、興味がある方はぜひ見てみましょう。

https://dev.epicgames.com/documentation/ja-jp/uefn/defer-in-verse

Discussion