※こちらは,会社の技術ブログとのクロスポスト記事です.元の記事はこちら
配列操作はプログラムの基本的な機能の一つですが,Go言語は他言語と比較してこの操作が使い難いと感じています.
この記事では,Go言語の配列操作の使い難さを改善した話をします.
問題
この記事では,3種類の基本的な配列操作を取り上げます.
- 配列末尾の要素を取得する
- 配列末尾に要素を追加する
- 配列のi番目の要素を削除する
C++, Python, MATLABでの操作を紹介し,その後Go言語ではどのような操作になるか解説します.
そして,Go言語の配列操作の使い難さを改善する方法を紹介します.
C++
C++の配列操作プログラムと出力は,以下の通りです.
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> nums = {0,1,2,3,4}; // 配列の宣言
for (auto num : nums) cout << num << " ";
cout << endl;
auto e = nums.back(); // 配列末尾の要素を取得する
cout << e << endl;
nums.push_back(8); // 配列末尾に要素を追加する
for (auto num : nums) cout << num << " ";
cout << endl;
nums.erase(nums.begin() + 3); // 配列のi番目の要素を削除する
for (auto num : nums) cout << num << " ";
cout << endl;
return 0;
}
0 1 2 3 4
4
0 1 2 3 4 8
0 1 2 4 8
C++は,コードが23行もあり,文字数も長いです.
配列末尾の要素を取得する操作back
は,数値を渡すことができません.
末尾からx番目の要素を取得するには,*(nums.crbegin()+x)
やend(nums)[-x]
を使う必要があります.
配列要素の削除は,配列名を2回書く必要があります.
Python
Pythonの配列操作プログラムと出力は,以下の通りです.
nums = list(range(5)) # 配列の宣言
print(nums)
e = nums[-1] # 配列末尾の要素を取得する
print(e)
nums.append(8) # 配列末尾に要素を追加する
print(nums)
nums.pop(3) # // 配列のi番目の要素を削除する
print(nums)
[0, 1, 2, 3, 4]
4
[0, 1, 2, 3, 4, 8]
[0, 1, 2, 4, 8]
Pythonは,C++よりもプログラムが簡潔です.
行数が短くなっており,push_back(8)
→append(8)
のように操作の名前も短くなっています.
nums[-x]
で,配列の末尾からx番目の要素を取得できます.
配列名を2回以上書く操作も存在しません.
MATLAB
MATLABの配列操作プログラムと出力は,以下の通りです.
nums = 0 : 4; % 配列の宣言
disp(nums);
e = nums(end); % 配列末尾の要素を取得する
disp(e);
nums = [nums 8]; % 配列末尾に要素を追加する
disp(nums);
nums(4) = []; % // 配列のi番目の要素を削除する
disp(nums);
0 1 2 3 4
4
0 1 2 3 4 8
0 1 2 4 8
MATLABは,Pythonと行数は同じで,文字数がより削減されています.
配列操作で覚える必要のある英単語は,配列末尾を表すend
のみです.
MATLABは,1:2:9
で1から9までの奇数配列を用意できるなど,配列の宣言がシンプルかつ強力です.
nums(end-x)
で配列の末尾からx+1番目の要素を取得できます.
配列末尾に要素を追加する操作は配列名を2回書く必要がありますが,配列名が短い場合は他言語より簡潔に記述できます.
また,同じ記述で配列どうしの連結ができます.
MATLABはindexが1始まりなので,左から4番目の要素を削除したい時は,数字の4を使います.
プログラムを機械的に日本語に翻訳する場合,PythonよりMATLABが分かり易いと思います.
- MATLAB
nums(4) = [];
の機械的翻訳:配列nums
の左から4
番目の要素をにする. - Python
nums.pop(3)
の機械的翻訳:配列nums
について,pop
(空,削除)する,左から3
番目の要素を.(一番左を0番目として考える)
MATLABは,自然な日本語「配列のi番目の要素を削除する」と記述プログラムが似ています.
Go言語
Go言語の配列操作プログラムと出力は,以下の通りです.
package main
import "fmt"
func main() {
nums := []int{0, 1, 2, 3, 4} // 配列の宣言
fmt.Println(nums)
e := nums[len(nums)-1] // 配列末尾の要素を取得する
fmt.Println(e)
nums = append(nums, 8) // 配列末尾に要素を追加する
fmt.Println(nums)
i := 4
nums = append(nums[:i], nums[i+1:]...) // 配列のi番目の要素を削除する
fmt.Println(nums)
nums = nums[:i+copy(nums[i:], nums[i+1:])] // 配列のi番目の要素を削除する
fmt.Println(nums)
}
[0 1 2 3 4]
4
[0 1 2 3 4 8]
[0 1 2 3 8]
[0 1 2 3]
Go言語は,配列操作時に配列名を2回以上書きます.他言語より1行の記述量が多いです.
配列末尾を表すものが存在しないので,[]
の中で配列末尾のindexを計算する必要があります.
len(nums)
で配列の長さを計算し,それを-1
してindexを求めます.(Go言語はindexが0始まりです)
Go言語で配列のi番目の要素を削除する場合,代表的な方法が2種類あります.
16行目の方法は,appendの仕様と配列のサブ集合操作がわかれば,初見でも理解できそうな方法です.
ただし,配列名を3回も書く必要があり,もう1つの方法より実行速度が遅いです.
19行目の方法は,配列名を4回も書く必要があり,操作も難解です.
配列の長さを超えた要素を暗黙の処理で切り落とすテクニックを使っています.
実行速度は,こちらの方が優位です.
他言語と比較してGo言語の基本的な配列操作が使い難いと考えているのですが,いかがでしょうか.
コードの分かり易さを重視して変数名を長くすると,ひどい目に遭います.
例)塵肺症患者データから,指定された特殊な患者のデータを削除する
pneumonoultramicroscopicsilicovolcanoconiosisPatientData = pneumonoultramicroscopicsilicovolcanoconiosisPatientData[:specifiedSpecialPatientIndex+copy(pneumonoultramicroscopicsilicovolcanoconiosisPatientData[specifiedSpecialPatientIndex:], pneumonoultramicroscopicsilicovolcanoconiosisPatientData[specifiedSpecialPatientIndex+1:])]
pneumonoultramicroscopicsilicovolcanoconiosis
は,辞書で見つけた最長の英単語です.
似た名前の変数が存在した場合,複数の変数が混在していたとしても発見が困難です.
配列名を4回,削除要素のindexを表す変数名を3回書くプログラムは,改善が必須と考えます.
改善したGo言語
改善したGo言語の配列操作プログラムは,以下の通りです.(出力は同じです)
package main
import "fmt"
func main() {
nums := []int{0, 1, 2, 3, 4} // 配列の宣言
fmt.Println(nums)
e := Back(&nums) // 配列末尾の要素を取得する
fmt.Println(e)
PushBack(&nums, 8) // 配列末尾に要素を追加する
fmt.Println(nums)
Erase(&nums, 4) // 配列のi番目の要素を削除する
fmt.Println(nums)
}
func Back[T any](slice *[]T) T {
a := *slice
return a[len(a)-1]
}
func PushBack[T any](slice *[]T, v T) {
*slice = append(*slice, v)
}
func Erase[T any](slice *[]T, i int) {
a := *slice
*slice = a[:i+copy(a[i:], a[i+1:])]
}
C++を参考に,3種類の関数を新しく実装しました.
そして,main関数をシンプルに書き換えました.
Go 1.18からGenericsが導入されたので,配列操作の汎用関数が簡単に書けるようになりました.
関数の引数をポインタにしたことで,main関数では配列名を1回書くだけで良くなっています.
終わりに
Go言語はシンプルさが特徴の言語です.
他言語に慣れていると使い難いと感じる部分もありますが,日々のアップデートにより,プログラムの自作で改善可能な部分も増えています.
自分は,Back
,PushBack
,Erase
のような汎用的に使える関数を別packageにまとめています.
配列と乱数を組み合わせた関数,DeepCopy関数,SubSet関数などもまとめて,便利に使っています.
プログラムが使い難いと感じた時は,創意工夫で乗り切りましょう.