【完了】コード評価依頼:undefinedとかnullの意味と扱い・メモタグの仕様

munokura

ユーザー
下記のプラグインのコードへの改善意見をいただきたく、お願いいたします。
基本的にRPGツクールMV向けに作っています。
(MZでも動作したので @target に両方を書いていますが)


1.タグの取得から処理分岐について
最初は、敵キャラのメモタグを取得するのに、タグがない処理を入れないで作成していました。
いきなりStringに突っ込んで"undefined"なら弾く…ような処理をしていましたが、「undefinedってファイル名も使えないって事だから、良くない書き方じゃない?」と考え、下記辺りを参考に色々試した(課程のコードは消してしまいました…)のですが、上手く行かず現状はnullを入れています。


2.タグの仕様について
あたりを参考に、SEのファイル名と再生パラメーターを分けて作りました。
パラメーターを省略できるというのもありましたが、大きくはデプロイメントを実行するときに[未使用ファイルを含まない]にチェックしていた場合に削除されないようにするために分けました。
この考えは合っているのでしょうか?

というのも、自分はRPGツクールMVで[未使用ファイルを含まない]がろくに機能した記憶がないので、当時使っていたプラグインが対応していなかったのか?タグの仕様でそうなっていたのか?よく分かっていません。

ご指導いただきたく、お願いいたします。
 
どうも、ムノクラさん。
僕はこれまで多数「新しいプラグインがセーブ後に呼び出されても動かせるようにする」を
モットーにしてきましたが、判定は、以下のように行っていました。

コード:
if (name == null) {
    console.log("名前が未設定です!");
}
通常は === を使うことが多いでしょうが、
== を使うと、undefined も null と同値とみなされるため、これで行けます。

また、メモ欄に<abc:1>ではなく、<abc>とだけ書くと、
そのオブジェクトの,meta.abc はtrueになります。

まだムノクラさんのご質問を完全に把握したくらいでの回答ですので、
これで問題が解決しないなら、より具体的なケースを示していただけると
大変助かります。
 

munokura

ユーザー
少々、解像度が低い質問文でしたね。
一度に質問しようとすると、こういうことになりますね…

シンプルな質問(2)の方から、具体的に書きます。
今回、メモタグを2つに分ける仕様にしました。

<CollapseSeFile:ファイル名>
<CollapseVPP:音量,ピッチ,位相>

もし、これを下記のように1つにまとめたとします。

<CollapseSe:ファイル名,音量,ピッチ,位相>

この場合に、下記のプラグインパラメーターを入れると、デプロイメントを実行する時で[未使用ファイルを含まない]にチェックしていた場合に削除されないようになりますか?

コード:
@noteParam CollapseSe
@noteRequire 1
@noteDir audio/se/
@noteType file
@noteData enemies
もしくは、パラメーターの書き方を変えることで、削除されないようにできますか?
 

fspace

ユーザー
短いコードだったのでとりあえず自分流に書き換えてみました。必ずしもいい書き方が決まっているわけではないので参考程度に。テストしてないので動かなかったらすみません……。

JavaScript:
const TAG_SE_FILE = "CollapseSeFile";
const TAG_VPP = "CollapseVPP";

const DEFAULT_VOLUME = 90;
const DEFAULT_PITCH = 100;
const DEFAULT_PAN = 0;

const parseVpp = s => s.trim().split(/\s*,\s*/).map(s => Number.parseInt(s, 10));
const parseSe = meta => {
    const name = meta[TAG_SE_FILE];
    const vpp = meta[TAG_VPP];
    if (typeof name === 'string') {
        const [
            volume = DEFAULT_VOLUME,
            pitch = DEFAULT_PITCH,
            pan = DEFAULT_PAN,
        ] = typeof vpp === 'string' ? parseVpp(vpp) : [];
        return { name, volume, pitch, pan };
    } else {
        return undefined;
    }
};

const Game_Enemy_performCollapse = Game_Enemy.prototype.performCollapse;
Game_Enemy.prototype.performCollapse = function () {
    const { meta } = this.enemy();
    const se = parseSe(meta);
    if (this.collapseType() === 0 && se !== undefined) {
        Game_Battler.prototype.performCollapse.apply(this, arguments);
        this.requestEffect('collapse');
        AudioManager.playSe(se);
    } else {
        Game_Enemy_performCollapse.apply(this, arguments);
    }
};
1.タグの取得から処理分岐について
最初は、敵キャラのメモタグを取得するのに、タグがない処理を入れないで作成していました。
いきなりStringに突っ込んで"undefined"なら弾く…ような処理をしていましたが、「undefinedってファイル名も使えないって事だから、良くない書き方じゃない?」と考え、下記辺りを参考に色々試した(課程のコードは消してしまいました…)のですが、上手く行かず現状はnullを入れています。
神無月さんの言う通り、メモタグを取得しようとすると、書き方やタグの有無によって文字列・ trueundefinedのいずれかの値が返ってきます。今回は文字列が指定された場合のみ処理したいので、typeof x === 'string'でまず文字列かどうかを判定して、文字列だった場合のみ内容を解析します。処理対象じゃなかった場合に条件演算子でnull(上記コードの場合はundefined)のような無効値を返すのはよく使われるテクニックでまったく問題ありません。

もし、これを下記のように1つにまとめたとします。

<CollapseSe:ファイル名,音量,ピッチ,位相>

この場合に、下記のプラグインパラメーターを入れると、デプロイメントを実行する時で[未使用ファイルを含まない]にチェックしていた場合に削除されないようになりますか?
試してはいませんが、おそらくならないと思います。「ファイル名,音量,ピッチ,位相」のうちどの部分がファイル名なのかを知る方法がないため、全体でひとつのファイル名だと判断されそうな気がします。

もしメモタグをシンプルにまとめたいのであれば、ファイルの設定をプラグインパラメータの方に移動してしまって、メモタグにはそれらを参照するIDだけ記述するというのも手かと思います。

JavaScript:
/*:
 * ...
 * @param SeList
 * @type struct<Se>[]
 * @default []
 * @text SE設定
 * @desc ...
 */
 
/*~struct~Se
 * @param id
 * @type string
 * @text ID
 * @desc 任意のIDを設定。
 *
 * @param name
 * @type file
 * @dir audio/se/
 * @text ファイル
 * @desc 再生するSEファイル。
 *
 * @param volume
 * ...
 */

// <CollapseSe: mySE>
ちなみに、undefinednullの使い分けについてですが、人によって扱い方が違うので結構面倒くさかったりします。

JavaScriptという言語においては、undefinedは任意の『値』に対する無効値、nullは任意の『オブジェクト』に対する無効値らしいです。JavaScriptにおいて『値』は大きく『プリミティブ』(数値、文字列、真偽値など)と『オブジェクト』に分かれます。undefinedは計算結果が『プリミティブ』か『オブジェクト』かわからない場合に、nullは計算結果が『オブジェクト』だとわかっている場合に、期待する結果が得られなかったことを示す値として使われます。このため、typeof undefined'undefined'になりますが、typeof null'object'になります。

ただ実際これらを使い分けることに利点があるかというと怪しくて、単に扱いが面倒になることも多いのでundefinedのみを使う、nullのみを使う、という書き方をする人も多いです。例えば、有名なTypeScriptというJavaScriptの拡張言語のコントリビュータ(開発協力者)向けのガイドラインには、undefinedのみを使用して、nullを使用しないことと書かれていたりします。

この辺りの使い分けはそれぞれ流儀はあっても些事なので、あまり細かく指摘されることはないとは思いますが、読み手を混乱させないためにも何らかのルールには従った方がいいと思います。
 

げれげれ

ユーザー
・・・横からすみません。
自分なりにコードから意図を読み取ってみたのですが、認識に相違ないかの確認と、
幾つか疑問点があるので質問をお願いしてもよろしいでしょうか。
もしかしたら、ムノクラさんが読み解く参考になるかもしれませんし。(方便)


【自分なりに読み取れた意図】
・タグ名、音に関する各数値はマジックナンバーに当たるので定数とした。
 (コード中のマジックナンバーはなるべく避ける)
・元の記述だと「Game_Enemy.prototype.performCollapse」内で全ての処理を行っていたが、
 タグの解析はそれ自体が一つの処理と見做すことができ、また、それなりの行数となるので、
 関数として独立させた。(処理はなるべく細分化するのがベター)
・(今回の場合)ファイル名の有無はtypeof演算子によって型で判断。
 (ここはちょっとTypeScriptの型ガードっぽい書き方をしてある)
・音の各数値の取得は「分割代入(規定値あり)」「三項演算子」を使って一行にすっきりまとめた。
 (vppが文字列型でなかった場合には空配列が返ることにより、規定値が適用される)

ここまでの認識はあっておりますでしょうか?

次に、疑問点です。

【疑問点】
・タグの解析で「split(',')」でなくあえて「split(/\s*,\s*/)」としてある意図がつかめませんでした。
 「/\s*,\s*/」 → 「“垂直タグでない空白文字が0文字以上”に挟まれたカンマ」の意図するところとは?
・「const { meta } = this.enemy();」の箇所について。
 要素一つだけを分割代入の形式で取得する意図は何でしょうか。
 metaの中身がオブジェクトであることが直感的にわかる点?

お手数ですが、お手すきの際にでもご確認いただけると幸いです。
 

fspace

ユーザー
・タグ名、音に関する各数値はマジックナンバーに当たるので定数とした。
 (コード中のマジックナンバーはなるべく避ける)
その通りです。

マジックナンバーと呼ぶほどかどうかは微妙ですが、変数に入れて名前を付けることで意味をわかりやすくしています。

・元の記述だと「Game_Enemy.prototype.performCollapse」内で全ての処理を行っていたが、
 タグの解析はそれ自体が一つの処理と見做すことができ、また、それなりの行数となるので、
 関数として独立させた。(処理はなるべく細分化するのがベター)
その通りです。

performCollapseと直接的には関係のない処理なので、関数として抽出し、名前を付けることで意味をわかりやすくしています。

・(今回の場合)ファイル名の有無はtypeof演算子によって型で判断。
 (ここはちょっとTypeScriptの型ガードっぽい書き方をしてある)
前述の通り、文字列・trueundefinedのうち文字列だけを対象とするためにtypeofを使っています。もう少し丁寧にやるのであれば、trueや空文字だった時にエラーを投げてもいいかもしれません。

TypeScriptはJavaScriptを静的な型解析のために拡張したもので、JavaScriptの記述がまず先にあるので、TypeScriptっぽい書き方と言われると少し変な感じがします。

・音の各数値の取得は「分割代入(規定値あり)」「三項演算子」を使って一行にすっきりまとめた。
 (vppが文字列型でなかった場合には空配列が返ることにより、規定値が適用される)
挙動についてはその通りです。

ここは書き方がいろいろあると思うので、これが特別いいコードだとは思っていません。もう少し丁寧に書くのであれば、数値以外が指定された場合や数値がすべて指定されていない場合にエラーを投げたりしてもいいと思います。

・タグの解析で「split(',')」でなくあえて「split(/\s*,\s*/)」としてある意図がつかめませんでした。
 「/\s*,\s*/」 → 「“垂直タグでない空白文字が0文字以上”に挟まれたカンマ」の意図するところとは?
/\s*,\s*/を指定する理由は,の前後の空白を除くためです。

「空白があってもNumber.parseIntでパースできるのでは?」という意味の質問であれば、全体としての結果は同じでも、それが意図通りのわかりやすい挙動ではないからです。例えば、Number.parseIntは後で別の関数によって置き換えられるかもしれませんし、パースの前にフォーマットが正しいかどうか確認をすることになるかもしれません。そういった場合に空白が残っているとうまく動かない可能性があります。

・「const { meta } = this.enemy();」の箇所について。
 要素一つだけを分割代入の形式で取得する意図は何でしょうか。
 metaの中身がオブジェクトであることが直感的にわかる点?
あまり深い意味はありませんが、個人的にメソッドチェーン以外で関数呼び出しの後にプロパティ参照を続けたくないというのと、変数名とプロパティ名で二回metaと書きたくないからです。

ちなみに、この形でmetaがオブジェクトかどうかはわかりません。
 

げれげれ

ユーザー
ご確認ありがとうございます。

前半部分の認識は概ね合っているようで良かったです。

型ガード云々はうがち過ぎでしたね。
まずJavaScriptの自然な記述としてこのような書き方が先にあり、
TypeScriptではそれを型ガードと呼んでいる、という理解で咀嚼できました。

/\s*,\s*/を指定する理由は,の前後の空白を除くためです。
「split(/\s*,\s*/) 」の実際の挙動がイメージできておりませんでした。
セパレータに任意文字数の空白文字を含めると、空白文字があった場合に除いてくれる…
言われてみればなるほどです。
正規表現に苦手意識があるのですが、ちょっとだけ前進できた気がします。

あまり深い意味はありませんが、個人的にメソッドチェーン以外で関数呼び出しの後にプロパティ参照を続けたくないというのと、変数名とプロパティ名で二回metaと書きたくないからです。
自分の力量だとまだあまりピンと来ていないのですが、そのような視点もあると認識した上で
コードに触れてみようと思います。
メソッドとプロパティが入れ替わり立ち代わり繋がっていたらなんかヤだな、
というのは、今の自分でもなんとなく理解できる気がします。

ちなみに、この形でmetaがオブジェクトかどうかはわかりません。
っと、確かにこれだと「オブジェクトから」データを取得したことは分かっても、
中身がどうかは判別できないですね。失礼しました。

諸々理解が深まりました。
ご確認ありがとうございました!
 

munokura

ユーザー
皆さん、ありがとうございます。
とても勉強になります。

直ぐに理解・習得とは行かないと思いますが、時間をかけて学習を進めます。
今後とも宜しくお願いいたします。
 
トップ