2026/08/04(火)PHPとPerlでは配列変数を変数に代入してもシャローコピーされない
私は今までプログラミング言語の鉄則として、プリミティブを代入すれば値が、オブジェクトを代入すれば参照が渡され、オブジェクトの実体を渡す場合にはディープコピーが必要だと考えていたが、PHPでは違うことに気が付き、常識を打ち破られたので備忘録として残す。
調べている中でPerlでもPHPと同じ振る舞いをすることに気づいた。
言語別の比較
以下は言語別に配列変数を別の変数に代入し、代入側変数を操作したときに、元の変数に影響を与えるか、つまり参照代入になるか調べた結果だ。
❌通常代入=プリミティブと同じ、✅参照代入=いわゆるポインタ渡しされる、という意味合いで書いている。頭の絵文字は視認性のために付けているだけで、特に意味はない。
| 言語 | PHP | Perl | Ruby | Python | JavaScript | C# |
|---|---|---|---|---|---|---|
| 配列 | ❌通常代入 | ❌通常代入 | ✅参照代入 | ✅参照代入 | ✅参照代入 | ✅参照代入 |
| 連想配列 | ❌通常代入 | ❌通常代入 | ✅参照代入 | ✅参照代入 | ✅参照代入 | ✅参照代入 |
| クラス | ✅参照代入 | ✅参照代入 | ✅参照代入 | ✅参照代入 | ✅参照代入 | ✅参照代入 |
PHPは公式ドキュメントにもそう書いてあった。
配列への代入においては、常に値がコピーされることに注意してください。 配列をリファレンスでコピーする場合には、 リファレンス演算子を使う必要があります。
そもそもPHPではオブジェクトと配列が別の概念として書かれているので、扱いが異なるのかもしれない。
各言語による処理結果
PHP
PHP 8.5.8で確認。
<?php
$arr1 = array('1', '2');
# ARR1 1, 2
echo "ARR1 {$arr1[0]}, {$arr1[1]}\n";
$arr2 = $arr1;
$arr2[0] = '3';
# ARR2 1, 2
echo "ARR2 {$arr1[0]}, {$arr1[1]}\n";
# ARR3 3, 2
echo "ARR3 {$arr2[0]}, {$arr2[1]}\n";
$hash1 = array(
'hoge' => 'a',
'fuga' => 'b'
);
# HASH1 a, b
echo "HASH1 {$hash1['hoge']}, {$hash1['fuga']}\n";
$hash2 = $hash1;
$hash2['hoge'] = 'z';
# HASH2 a, b
echo "HASH2 {$hash1['hoge']}, {$hash1['fuga']}\n";
# HASH3 z, b
echo "HASH3 {$hash2['hoge']}, {$hash2['fuga']}\n";
$cls1 = new stdClass();
$cls1->hoge = 'A';
$cls1->fuga = 'B';
# CLS1 A, B
echo "CLS1 {$cls1->hoge}, {$cls1->fuga}\n";
$cls2 = $cls1;
$cls2->hoge = 'Q';
# CLS2 Q, B
echo "CLS2 {$cls1->hoge}, {$cls1->fuga}\n";
# CLS3 Q, B
echo "CLS3 {$cls2->hoge}, {$cls2->fuga}\n";
# ARR1 1, 2
# ARR2 1, 2
# ARR3 3, 2
# HASH1 a, b
# HASH2 a, b
# HASH3 z, b
# CLS1 A, B
# CLS2 Q, B
# CLS3 Q, B
Perl
Perl v5.38.2で確認。
use strict;
use warnings;
my @arr1 = ('1', '2');
# ARR1 1, 2
print "ARR1 $arr1[0], $arr1[1]\n";
my @arr2 = @arr1;
$arr2[0] = '3';
# ARR2 1, 2
print "ARR2 $arr1[0], $arr1[1]\n";
# ARR3 3, 2
print "ARR3 $arr2[0], $arr2[1]\n";
my %hash1 = (
'hoge' => 'a',
'fuga' => 'b'
);
# HASH1 a, b
print "HASH1 $hash1{'hoge'}, $hash1{'fuga'}\n";
my %hash2 = %hash1;
$hash2{'hoge'} = 'z';
# HASH2 a, b
print "HASH2 $hash1{'hoge'}, $hash1{'fuga'}\n";
# HASH3 z, b
print "HASH3 $hash2{'hoge'}, $hash2{'fuga'}\n";
my $cls1 = bless {
hoge => 'A',
fuga => 'B'
}, 'StdClass';
$cls1->{'hoge'} = 'A';
$cls1->{'fuga'} = 'B';
# CLS1 A, B
print "CLS1 $cls1->{hoge}, $cls1->{fuga}\n";
my $cls2 = $cls1;
$cls2->{'hoge'} = 'Q';
# CLS2 Q, B
print "CLS2 $cls1->{hoge}, $cls1->{fuga}\n";
# CLS3 Q, B
print "CLS3 $cls2->{hoge}, $cls2->{fuga}\n";
# ARR1 1, 2
# ARR2 1, 2
# ARR3 3, 2
# HASH1 a, b
# HASH2 a, b
# HASH3 z, b
# CLS1 A, B
# CLS2 Q, B
# CLS3 Q, B
Ruby
Ruby 4.0.5で確認。
arr1 = ['1', '2']
# ARR1 1, 2
puts "ARR1 #{arr1[0]}, #{arr1[1]}"
arr2 = arr1
arr2[0] = '3'
# ARR2 3, 2
puts "ARR2 #{arr1[0]}, #{arr1[1]}"
# ARR3 3, 2
puts "ARR3 #{arr2[0]}, #{arr2[1]}"
hash1 = {
'hoge' => 'a',
'fuga' => 'b'
}
# HASH1 a, b
puts "HASH1 #{hash1['hoge']}, #{hash1['fuga']}"
hash2 = hash1
hash2['hoge'] = 'z'
# HASH2 z, b
puts "HASH2 #{hash1['hoge']}, #{hash1['fuga']}"
# HASH3 z, b
puts "HASH3 #{hash2['hoge']}, #{hash2['fuga']}"
class HogeCls
attr_accessor :hoge, :fuga
def initialize(_hoge, _fuga)
@hoge = _hoge
@fuga = _fuga
end
end
cls1 = HogeCls.new('A', 'B')
# CLS1 A, B
puts "CLS1 #{cls1.hoge}, #{cls1.fuga}"
cls2 = cls1
cls2.hoge = 'Q'
# CLS2 Q, B
puts "CLS2 #{cls1.hoge}, #{cls1.fuga}"
# CLS3 Q, B
puts "CLS3 #{cls2.hoge}, #{cls2.fuga}"
# ARR1 1, 2
# ARR2 3, 2
# ARR3 3, 2
# HASH1 a, b
# HASH2 z, b
# HASH3 z, b
# CLS1 A, B
# CLS2 Q, B
# CLS3 Q, B
Python
Python 3.12.0で確認。
arr1 = ['1', '2']
# ARR1 1, 2
print(f"ARR1 {arr1[0]}, {arr1[1]}")
arr2 = arr1
arr2[0] = '3'
# ARR2 3, 2
print(f"ARR2 {arr1[0]}, {arr1[1]}")
# ARR3 3, 2
print(f"ARR3 {arr2[0]}, {arr2[1]}")
hash1 = {
'hoge': 'a',
'fuga': 'b'
}
# HASH1 a, b
print(f"HASH1 {hash1['hoge']}, {hash1['fuga']}")
hash2 = hash1
hash2['hoge'] = 'z'
# HASH2 z, b
print(f"HASH2 {hash1['hoge']}, {hash1['fuga']}")
# HASH3 z, b
print(f"HASH3 {hash2['hoge']}, {hash2['fuga']}")
class HogeCls:
def __init__(self, _hoge, _fuga):
self.hoge = _hoge
self.fuga = _fuga
cls1 = HogeCls('A', 'B')
# CLS1 A, B
print(f"CLS1 {cls1.hoge}, {cls1.fuga}")
cls2 = cls1
cls2.hoge = 'Q'
# CLS2 Q, B
print(f"CLS2 {cls1.hoge}, {cls1.fuga}")
# CLS3 Q, B
print(f"CLS3 {cls2.hoge}, {cls2.fuga}")
# ARR1 1, 2
# ARR2 3, 2
# ARR3 3, 2
# HASH1 a, b
# HASH2 z, b
# HASH3 z, b
# CLS1 A, B
# CLS2 Q, B
# CLS3 Q, B
JavaScript
Node.js v24.16.0、Microsoft Edge 150.0.4078.105で確認。
const arr1 = ['1', '2'];
// ARR1 1, 2
console.log(`ARR1 ${arr1[0]}, ${arr1[1]}`);
const arr2 = arr1;
arr2[0] = '3';
// ARR2 3, 2
console.log(`ARR2 ${arr1[0]}, ${arr1[1]}`);
// ARR3 3, 2
console.log(`ARR3 ${arr2[0]}, ${arr2[1]}`);
const hash1 = {
hoge: 'a',
fuga: 'b'
};
// HASH1 a, b
console.log(`HASH1 ${hash1['hoge']}, ${hash1['fuga']}`);
const hash2 = hash1;
hash2['hoge'] = 'z';
// HASH2 z, b
console.log(`HASH2 ${hash1['hoge']}, ${hash1['fuga']}`);
// HASH3 z, b
console.log(`HASH3 ${hash2['hoge']}, ${hash2['fuga']}`);
class HogeCls {
constructor(_hoge, _fuga) {
this.hoge = _hoge
this.fuga = _fuga
}
}
const cls1 = new HogeCls('A', 'B');
// CLS1 A, B
console.log(`CLS1 ${cls1.hoge}, ${cls1.fuga}`);
const cls2 = cls1;
cls2.hoge = 'Q';
// CLS2 Q, B
console.log(`CLS2 ${cls1.hoge}, ${cls1.fuga}`);
// CLS3 Q, B
console.log(`CLS3 ${cls2.hoge}, ${cls2.fuga}`);
// ARR1 1, 2
// ARR2 3, 2
// ARR3 3, 2
// HASH1 a, b
// HASH2 z, b
// HASH3 z, b
// CLS1 A, B
// CLS2 Q, B
// CLS3 Q, B
C#
バージョンの基準が謎だが、Visual Studio 2022の標準的な環境で確認した。
var arr1 = new[] { "1", "2" };
// ARR1 1, 2
Console.WriteLine($"ARR1 {arr1[0]}, {arr1[1]}");
var arr2 = arr1;
arr2[0] = "3";
// ARR2 3, 2
Console.WriteLine($"ARR2 {arr1[0]}, {arr1[1]}");
// ARR3 3, 2
Console.WriteLine($"ARR3 {arr2[0]}, {arr2[1]}");
var hash1 = new Dictionary<string, string> {
["hoge"] = "a",
["fuga"] = "b"
};
// HASH1 a, b
Console.WriteLine($"HASH1 {hash1["hoge"]}, {hash1["fuga"]}");
var hash2 = hash1;
hash2["hoge"] = "z";
// HASH2 z, b
Console.WriteLine($"HASH2 {hash1["hoge"]}, {hash1["fuga"]}");
// HASH3 z, b
Console.WriteLine($"HASH3 {hash2["hoge"]}, {hash2["fuga"]}");
var cls1 = new HogeCls("A", "B");
// CLS1 A, B
Console.WriteLine($"CLS1 {cls1.Hoge}, {cls1.Fuga}");
var cls2 = cls1;
cls2.Hoge = "Q";
// CLS2 Q, B
Console.WriteLine($"CLS2 {cls1.Hoge}, {cls1.Fuga}");
// CLS3 Q, B
Console.WriteLine($"CLS3 {cls2.Hoge}, {cls2.Fuga}");
class HogeCls {
public string Hoge { get; set; }
public string Fuga { get; set; }
public HogeCls(string hoge, string fuga) {
this.Hoge = hoge;
this.Fuga = fuga;
}
}
// ARR1 1, 2
// ARR2 3, 2
// ARR3 3, 2
// HASH1 a, b
// HASH2 z, b
// HASH3 z, b
// CLS1 A, B
// CLS2 Q, B
// CLS3 Q, B
あとがき
PHPのクラスなんて連想配列の派生形だと思ってたら、PHPではクラスこそがオブジェクトということで大変驚いた。一般論としては文字列・数値・真偽型がプリミティブ、それ以外の配列や連想配列(ハッシュ)・クラス・構造体はオブジェクトとして考えていただけに強く常識を打ち破られた気持ちだ。
とはいえ、配列は構造体ではないはずなので、構造体から派生するクラスと配列が別になるという思想そのものもわからなくはない。わからなくはないけど気持ち悪い。
PHPも進化しているので将来的には見直されるかもしれないし、されないかもしれない。後方互換性をしょっちゅう切り捨ててる印象があるので、何があっても不思議がないのも、またPHPだと思う。そしてPerlは恐らく変わらないだろう。
しかしいわゆるLL、軽量プログラミング言語と呼ばれているPerl、PHP、Ruby、Pythonのうち、RubyとPythonは違う振る舞いを示したのも興味深かった。一体何があったのか…。
2026/02/22(日)COM3D2のキャラデータをカスタムキャストで使う方法
カスタムキャスト側でCOM3D2のプリセットを再現しようとすると、色周りの設定が数値指定できない関係で難しすぎたので、何とかしようとして見つけた方法。
一撃で同じキャラを持ってこれるのでだいぶ楽。
必要なもの
- スマホとPCを繋ぐためのUSBケーブル
- バイナリエディタ
- COM3D2
- カスタムキャスト
確認環境
| Env | Ver |
|---|---|
| Android | 16 |
| COM3D2 | 2.29.4 |
| カスタムキャスト | 1.04.01 |
やり方
- スマホをUSBケーブルでPCに繋ぐ
- スマホ側の接続形態を充電からファイル転送モードにする
- カスタムキャストを開いて適当に服・体プリセットをセーブする
内部共有ストレージ/Android/data/jp.customcast.cc2/files/Presetを開き、保存したプリセットを取り出す- COM3D2を開き、カスタムキャストで使いたいキャラのエディット画面を開き、服・体プリセットを保存する
- バイナリエディタでCOM3D2のプリセットファイルを開き、
454E44AE426082を検索し、そこより先にあるバイナリコードをすべてコピーする

- カスタムキャストのプリセットを開き、
454E44AE426082を検索し、そこより先にあるバイナリコードを、先ほどコピーしたCOM3D2のバイナリコードですべて上書きし、保存する - 保存したカスタムキャストのプリセットファイルを
内部共有ストレージ/Android/data/jp.customcast.cc2/files/Presetに戻す - カスタムキャストを開き、キャラエディットからプリセットをロードする
備考
恐らくファイルヘッダ以外ほぼ互換だと思われるが、ヘッダのリプレースが面倒なのでボディを書き換えることで達成した。
当たり前だがMODとかは反映されない。
おまけ
データを個別に取得して弄れるようにしようとツールを書こうとしていたが、パラメーターが多いのと、フォーマットが異なるものがあり、解析に飽きたので調べた残骸として残しておく。
using System.Diagnostics;
using System.Text;
class Program {
static void Main(string[] args) {
string filePath = @"C:\path\to\pre_Dummymaid_20260222134142.preset";
if (!File.Exists(filePath)) {
Debug.WriteLine($"ファイルが見つかりません: {filePath}");
return;
}
byte[] fileData = File.ReadAllBytes(filePath);
Debug.WriteLine($"ファイル読み込み完了: {fileData.Length} bytes");
var obj = new ParamFinder(fileData);
var MuneL = obj.FindBodyValue("MuneL");
var MuneS = obj.FindBodyValue("MuneS");
var MuneTare = obj.FindBodyValue("MuneTare");
var RegFat = obj.FindBodyValue("RegFat");
var Hara = obj.FindBodyValue("Hara");
var RegMeet = obj.FindBodyValue("RegMeet");
var KubiScl = obj.FindBodyValue("KubiScl");
var UdeScl = obj.FindBodyValue("UdeScl");
var EyeSclX = obj.FindBodyValue("EyeSclX");
var EyeSclY = obj.FindBodyValue("EyeSclY");
var EyePosX = obj.FindBodyValue("EyePosX");
var EyePosY = obj.FindBodyValue("EyePosY");
var EyeClose = obj.FindBodyValue("EyeClose");
var EyeBallPosX = obj.FindBodyValue("EyeBallPosX");
var EyeBallPosY = obj.FindBodyValue("EyeBallPosY");
var EyeBallSclX = obj.FindBodyValue("EyeBallSclX");
var EyeBallSclY = obj.FindBodyValue("EyeBallSclY");
var FaceShape = obj.FindBodyValue("FaceShape");
var MayuY = obj.FindBodyValue("MayuY");
var HeadX = obj.FindBodyValue("HeadX");
var HeadY = obj.FindBodyValue("HeadY");
var DouPer = obj.FindBodyValue("DouPer");
var Sintyou = obj.FindBodyValue("sintyou");
var Koshi = obj.FindBodyValue("koshi");
var Kata = obj.FindBodyValue("kata");
var West = obj.FindBodyValue("west");
var MuneUpDown = obj.FindBodyValue("MuneUpDown");
var MuneYori = obj.FindBodyValue("MuneYori");
Debug.WriteLine($"MuneL: {MuneL}, MuneS: {MuneS}, MuneTare: {MuneTare}, RegFat: {RegFat}, Hara: {Hara}, RegMeet: {RegMeet}, KubiScl: {KubiScl}, UdeScl: {UdeScl}, EyeSclX: {EyeSclX}, EyeSclY: {EyeSclY}, EyePosX: {EyePosX}, EyePosY: {EyePosY}, EyeClose: {EyeClose}, EyeBallPosX: {EyeBallPosX}, EyeBallPosY: {EyeBallPosY}, EyeBallSclX: {EyeBallSclX}, EyeBallSclY: {EyeBallSclY}, FaceShape: {FaceShape}, MayuY: {MayuY}, HeadX: {HeadX}, HeadY: {HeadY}, DouPer: {DouPer}, Sintyou: {Sintyou}, Koshi: {Koshi}, Kata: {Kata}, West: {West}, MuneUpDown: {MuneUpDown}, MuneYori: {MuneYori}");
}
}
class ParamFinder {
private Byte[] FileData;
static byte[] CreateBinaryStringBytes(string text) {
using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms, Encoding.UTF8);
writer.Write(text);
writer.Flush();
return ms.ToArray();
}
/// <summary>
/// バイト配列内から指定パターンの最初の出現位置を返す (見つからなければ -1)
/// </summary>
static int FindPattern(byte[] data, byte[] pattern) {
int limit = data.Length - pattern.Length;
for (int i = 0; i <= limit; i++) {
bool match = true;
for (int j = 0; j < pattern.Length; j++) {
if (data[i + j] != pattern[j]) {
match = false;
break;
}
}
if (match)
return i;
}
return -1;
}
public ParamFinder(Byte[] fileData) {
this.FileData = fileData;
}
public int FindBodyValue(string subject) {
byte[] signature = CreateBinaryStringBytes(subject);
int sigOffset = FindPattern(this.FileData, signature);
if (sigOffset < 0) {
throw new Exception($"エラー: データセクションのシグネチャ '${subject}' が見つかりませんでした。");
}
using var ms = new MemoryStream(this.FileData, sigOffset, this.FileData.Length - sigOffset);
using var reader = new BinaryReader(ms, Encoding.UTF8);
reader.ReadString();
reader.ReadString();
reader.ReadInt32();
reader.ReadInt32();
reader.ReadString();
reader.ReadInt32();
reader.ReadInt32();
return reader.ReadInt32();
}
}

