質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.10%

GASで多次元配列の編集が上手くいかない件

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 6,108
退会済みユーザー

退会済みユーザー

前提・実現したいこと

Google Apps Scriptで、多次元配列をarray[i][j]で編集したい。
一方、他のプロジェクトファイルで同等のコードを実行した場合は上手くいく。(本当に謎)

発生している問題・エラーメッセージ

通常なら、簡易モデルですが[[0,0,0,0,0][0,0,1,0,0][0,0,0,0,0]]となるはずが、[[0,0,1,0,0][0,0,1,0,0][0,0,1,0,0]]となってしまいます。

該当のソースコード

function main(n) {
  //-----------------------------------------------

  n=11;
  //とりあえずnの値を代入

  var maze = [];
  var array = [];

  for(var j = 0; j < n; j++) {
      array.push(0);
  }
  //arrayに[0,0,..](0が11個)を代入

  for(var i = 0; i < n; i++) {
    maze.push(array);
  }
  //mazeに[array,array,...](arrayが11個)を代入


  //-----------------------------------------------

  var start = [Math.floor(Math.random() * (n - 1) / 2) * 2 + 1, Math.floor(Math.random() * (n - 1) / 2) * 2 + 1];
  //startに[1から9までの奇数,1から9までの奇数]を代入

  Logger.log(start[0]+"+"+start[1]);

  maze[start[0]][start[1]] = 1;
  //2次元配列mazeを書き換え

  Logger.log(maze);
}

試したこと

書き換えをrewrite関数(自作)に置き換えても結局ダメだった

補足情報(FW/ツールのバージョンなど)

ここにより詳細な情報を記載してください。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • dice142

    2018/12/07 13:03

    どういう出力を期待していて、現状どういう出力になっているのか追記してください。

    キャンセル

  • dice142

    2018/12/07 13:03

    失礼しました。ありましたね。

    キャンセル

回答 3

checkベストアンサー

+5

既に良い感じの回答が出てますが、
もう少し深掘りして原理の方を解説します。

console.log([1, 2, 3] === [1, 2, 3]); // false

var a = [1, 2, 3];
var b = a;
console.log(a === b); // true

不思議ですね、どうしてこういう動きするか説明出来ますか?
多分説明出来ないと思います。


JavaScriptの世界では、{name: "hoge"}というオブジェクトや、[1, 2, 3]という配列を宣言した時、初めてメモリ空間上にオブジェクト/配列が生成されます。
変数や引数として使う時は全て「メモリ空間のアドレス番地」でやりとりされます(参照やポインタ等と表現されます)
変数に代入された時、目には見えませんしとりだせませんが、オブジェクト/配列の実体ではなく、アドレス番地という数値が代入されていると考えてください。

[1, 2, 3] === [1, 2, 3]の結果がfalseになるのは、比較演算子でオブジェクトや配列を比較した場合、配列そのものではなく、「アドレス番地」での比較になります。
1行内に配列宣言が2個ありますので、メモリ空間上に配列2個作られ、それぞれのアドレス番地が帰って来ます。
各々のアドレス番地は当然異なる為、比較結果もfalseになるというカラクリになっています。

これがどう質問文に影響してくるかを見ていきましょう。

  var n = 11;
  var array = [];

  for(var j = 0; j < n; j++) {
      array.push(0);
  }

このvar array = [];で配列を1個生成しました。
for文で配列にぽこぽこ0を挿入していき0を11個含む配列に成長しました。

  for(var i = 0; i < n; i++) {
    maze.push(array);
  }

問題の箇所がこれです。
maze.push(array)としていますが、arrayには配列のアドレス番地が入ってます。
つまり、11回繰り返した結果mazeは[arrayのアドレス番地, arrayのアドレス番地, arrayのアドレス番地, ...]という配列になります。

理想はメモリ空間上にmazeとarrayのコピー11個、計12個の配列が存在してmazeの中身は[array1, array2, array3 ...]という風になって欲しいはずですが、
実際にはmazeとarrayの計2個の配列しか存在していません。
そりゃarrayの値を1個弄ったら全部変化するよね……という訳で、そのコードは欠陥となるわけです。

配列は基本的に宣言したタイミングで作られるので、
素直に解決するなら多重ループに変更してこういったコードになるでしょう。

function main (n) {
  n = 11;
  var maze = [];
  for (var i = 0; i < n; i++) {
    // ループの中で新しい配列を作って育てる
    var array = [];
    for (var j = 0; j < n; j++) {
      array.push(0);
    }
    maze.push(array);
  }
}

ですが、ループやif文のネストって理解が難しくなるのでできれば避けたいですよね。
そういった時に、メモリ空間上に配列の複製を作ってしまう手法があり、
これをシャローコピーやディープコピーと呼びます。
詳しい記事を見つけたので、具体的な方法はこちらを読んでみてください。

JavaScript の配列のシャローコピーに使えるメソッド3つ(+ 1つ)とその使い分け - おかかウェブ

回答文に上がっている解決策はこのシャローコピーやディープコピーの手法です。
(この手法はどちらもちゃんとGASで使える事を確認しています)

  • JSON.parse(JSON.stringify(...)): 簡易的なディープコピー
  • array.slice(): スマートなシャローコピー

【おまけ】【修正版】関数作ってワンライナーで頑張る

一撃で行けると思ったのですが、どうもGASはJavaScript1.6ベースでできており、
ECMAScript6の便利機能は全滅、5も動作が怪しいので、注意してください。

参考サイト

そもそも配列の要素を一気につくるイディオムがES6に頼る事になるので、
それに頼れないとなると一気に微妙化しますが、こんな感じで配列作る関数を用意して動作させることになります。
下記のコードは調査済みで動作します…が、array.slice()の方が明らかに筋が良いですね。参考までにどうぞ。

function myFunction() {
  var n = 11;
  var array = function (num) {
    var arr = [];
    for (var i = 0; i < num; i++) {
      arr.push(0);
    }
    return arr;
  }
  var maze = array(n).map(function () { return array(n); });
  Logger.log(maze);
}

// [18-12-07 17:36:31:549 JST] [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/07 16:47

    確かにおまけのところはGASでは無理そうですね...確認不足でしたごめんなさい。

    キャンセル

  • 2018/12/07 17:20

    苦戦しましたが見つけてきました。
    https://developers.google.com/apps-script/guides/services/#basic_javascript_features
    > Apps Script is based on JavaScript 1.6, plus a few features from 1.7 and 1.8.
    和訳すると「Apps ScriptはJavaScript1.6に対応、更に一部1.7や1.8にも対応しています!」
    で、JavaScript1.nがどう関わってくるかをWikipediaで調べたら、どうもやっとECMAScript5に準拠できたのがJavaScript1.9であり、それ未満である1.8以下というのは相当ヤバいですねこれは……
    https://ja.wikipedia.org/wiki/JavaScript

    おまけどころか1行イディオムは全部アウトで、
    `Logger.log(Object.getOwnPropertyNames(Array.prototype))`
    で持っているメソッド一覧を確認したら
    [constructor, toString, toLocaleString, toSource, join, reverse, sort, push, pop, shift, unshift, splice, concat, slice, indexOf, lastIndexOf, every, filter, forEach, map, some, reduce, reduceRight, length]
    とのことで、現行のJavaScriptからはかなり遅れているようです。回答を編集して書き直しておきます。

    https://stackoverflow.com/questions/39317252/what-functions-do-arrays-in-google-apps-script-support

    キャンセル

  • 2018/12/07 17:23

    すごい。この調査力。「Rhinoだ(推測)」みたいな記事まで読んであきらめてました。

    キャンセル

+4

  for(var i = 0; i < n; i++) {
    maze.push(array);
  }


このarrayは同じオブジェクトなので、mazeには同じオブジェクトをn個入れていることになります。
どれか書き換えると他の箇所も同様に変わっているので、期待したとおりにならないかと思います。

arrayをそれぞれ別なオブジェクトとして入れるようにすれば良いので、
都度数値を生成してもいいでしょうし、
JSON.parse(JSON.stringify(...))で文字列化→オブジェクト化してもできると思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/07 14:47

    var array = [];
    で一つの配列を生成していて、[]が新しい配列オブジェクトになります。
    つまりこの部分をfor文の中で行えば都度オブジェクトが生成されることになります。
    外側のループは配列の中に入れる配列の数(mazeの中のarrayの数)
    内側のループは中の配列に入れる要素数(各arrayの要素数)
    と二重ループで行えば都度生成された配列をmazeに入れることができます。

    キャンセル

  • 2018/12/07 15:53 編集

    横から失礼。0などの初期値でフィルされた配列が欲しいのであれば、↓こういう関数を作るしかないです。(gasはjsの仕様が古いので)

    function array_fill(n, defVal) {
    var ret = [];
    for(var i = 0; i < n; i++) {
    ret.push(defVal);
    }
    return ret;
    }

    使うときは、↓こう
    maze.push(array_fill(n, 0));

    キャンセル

  • 2018/12/07 16:34

    dice142さん、回答と丁寧な対応ありがとうございました!

    キャンセル

+3

同じ配列を参照してます。

安直ですが、↓したらどうでしょう?

maze.push(array.slice());

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.10%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る