はじめに
基本は大事ということで、Solidityを使う機会が増えてきたので、型(Types)のドキュメントをGoogle翻訳やDeepL翻訳を使用して翻訳してみます。
※:長いので記事を4分割します。
その1、その2、その3、その4
※:誤字脱字、翻訳ミスなどありましたら、お知らせくださいm(__)m
配列(Arrays)
原文
Arrays can have a compile-time fixed size, or they can have a dynamic size.
The type of an array of fixed size k
and element type T
is written as T[k]
, and an array of dynamic size as T[]
.
For example, an array of 5 dynamic arrays of uint
is written as uint[][5]
.
The notation is reversed compared to some other languages.
In Solidity, X[3]
is always an array containing three elements of type X
, even if X
is itself an array.
This is not the case in other languages such as C.
Indices are zero-based, and access is in the opposite direction of the declaration.
For example, if you have a variable uint[][5] memory x
, you access the seventh uint
in the third dynamic array using x[2][6]
, and to access the third dynamic array, use x[2]
.
Again, if you have an array T[5] a
for a type T
that can also be an array, then a[2]
always has type T
.
Array elements can be of any type, including mapping or struct. The general restrictions for types apply, in that mappings can only be stored in the storage
data location and publicly-visible functions need parameters that are ABI types.
It is possible to mark state variable arrays public
and have Solidity create a getter. The numeric index becomes a required parameter for the getter.
Accessing an array past its end causes a failing assertion. Methods .push()
and .push(value)
can be used to append a new element at the end of a dynamically-sized array, where .push()
appends a zero-initialized element and returns a reference to it.
Note
Dynamically-sized arrays can only be resized in storage.
In memory, such arrays can be of arbitrary size but the size cannot be changed once an array is allocated.
配列は、コンパイル時に固定サイズにすることも、動的サイズにすることもできます。
サイズ k
で要素の型が T
である固定サイズの配列は T[k]
と表記し,動的サイズの配列は T[]
と表記します.
例えば,動的配列 uint
を5個並べた配列は uint[][5]
と表記される.
他のいくつかの言語と比較すると、表記が逆になっています。
Solidity では、 X
が配列であっても、 X[3]
は常に X
型の要素を 3 つ含む配列となります。
これは C のような他の言語には当てはまりません。
インデックスはゼロから始まり、アクセスは宣言の反対方向です。
例えば、変数 uint[][5] memory x
がある場合、3番目の動的配列の7番目の uint
には x[2][6]
を用いてアクセスし、3番目の動的配列にアクセスするには x[2]
を用います。
繰り返しますが、配列にもなり得る型 T
の配列 T[5] a
がある場合、 a[2]
は常に型 T
を持っています。
配列の要素はマッピングや構造体など、どのような型でも構いません。マッピングは storage
というデータロケーションにのみ格納することができ、一般に公開されている関数は ABI 型のパラメータを必要とするという、型に関する一般的な制約が適用されます。
ステート変数配列を public
とマークし、Solidity にgetterを作成させることができます。数値インデックスがgetterの必須パラメーターになります。
配列の終端を越えてアクセスすると、アサーションに失敗します。メソッド .push()
と .push(value)
は、動的なサイズの配列の末尾に新しい要素を追加するために使用できます。ここで .push()
はゼロから初期化した要素を追加し、その参照を返します。
Note
動的サイズの配列は、ストレージ内でのみサイズ変更可能です。
メモリ上では、このような配列は任意のサイズにすることができますが、配列が割り当てられると、サイズを変更することはできません。
配列としてのbytes
と string
(bytes
and string
as Arrays)
原文
Variables of type bytes
and string
are special arrays.
The bytes
type is similar to bytes1[]
, but it is packed tightly in calldata and memory.
string
is equal to bytes
but does not allow length or index access.
Solidity does not have string manipulation functions, but there are third-party string libraries. You can also compare two strings by their keccak256-hash using keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
and concatenate two strings using string.concat(s1, s2)
.
You should use bytes
over bytes1[]
because it is cheaper, since using bytes1[]
in memory
adds 31 padding bytes between the elements.
Note that in storage
, the padding is absent due to tight packing, see bytes and string.
As a general rule, use bytes
for arbitrary-length raw byte data and string
for arbitrary-length string (UTF-8) data.
If you can limit the length to a certain number of bytes, always use one of the value types bytes1
to bytes32
because they are much cheaper.
Note
If you want to access the byte-representation of a string s
, use bytes(s).length
/ bytes(s)[7] = 'x'
;.
Keep in mind that you are accessing the low-level bytes of the UTF-8 representation, and not the individual characters.
bytes
型と string
型の変数は特殊な配列です。
bytes
型は bytes1[]
に似ていますが、calldata と memory にしっかりと格納されます。
string
は bytes
と同じですが、長さやインデックスへのアクセスはできません。
Solidity には文字列操作関数はありませんが、サードパーティの文字列ライブラリがあります。 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
を使用して keccak256-hash で 2 つの文字列を比較し、string.concat(s1, s2)
を使用して 2 つの文字列を連結することもできます。
bytes1[]
を memory
で使用すると、要素間に31バイトのパディングが追加されます。
storage
は、タイトパッキングのためパディングがないことに注意してください。 (bytes と string を参照)
一般的なルールとして、任意の長さの生のバイトデータには bytes
を、任意の長さの文字列 (UTF-8) のデータには string
を使用します。
もし、長さをあるバイト数に制限できるのであれば、常に bytes1
から bytes32
のいずれかの値型を使用すると、より安価になります。
Note
文字列 s
のバイト表現にアクセスしたい場合は、 bytes(s).length
/ bytes(s)[7] = 'x'
; を使用します。
このとき、個々の文字ではなく、UTF-8表現の低レベルのバイトにアクセスしていることに注意してください。
関数 bytes.concat
と string.concat
(The functions bytes.concat
and string.concat
)
原文
You can concatenate an arbitrary number of string
values using string.concat
.
The function returns a single string memory
array that contains the contents of the arguments without padding.
If you want to use parameters of other types that are not implicitly convertible to string
, you need to convert them to string
first.
Analogously, the bytes.concat
function can concatenate an arbitrary number of bytes
or bytes1 ... bytes32
values.
The function returns a single bytes memory
array that contains the contents of the arguments without padding.
If you want to use string parameters or other types that are not implicitly convertible to bytes
, you need to convert them to bytes
or bytes1
/…/bytes32
first.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
contract C {
string s = "Storage";
function f(bytes calldata bc, string memory sm, bytes16 b) public view {
string memory concatString = string.concat(s, string(bc), "Literal", sm);
assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concatString).length);
bytes memory concatBytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b);
assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concatBytes.length);
}
}
If you call string.concat
or bytes.concat
without arguments they return an empty array.
任意の数の string
値を連結するには、 string.concat
を使用します。
この関数は、引数の内容をパディングせずに格納した 1 つの string memory
配列を返します。
もし、暗黙のうちに string
に変換されない他の型のパラメータを使用したい場合は、最初に string
に変換する必要があります。
同様に、 bytes.concat
関数は、任意の数の bytes
や bytes1 ... bytes32
を連結して使用することができます。
この関数は、引数の内容をパディングせずに格納した 1 つの bytes memory
配列を返します。
文字列のパラメータや、暗黙のうちに bytes
に変換されない他の型を使用したい場合は、最初に bytes
または bytes1
/.../bytes32
に変換する必要があります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
contract C {
string s = "Storage";
function f(bytes calldata bc, string memory sm, bytes16 b) public view {
string memory concatString = string.concat(s, string(bc), "Literal", sm);
assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concatString).length);
bytes memory concatBytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b);
assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concatBytes.length);
}
}
引数なしで string.concat
や bytes.concat
を呼び出すと、空の配列が返されます。
メモリー配列の割り当て(Allocating Memory Arrays)
原文
Memory arrays with dynamic length can be created using the new
operator.
As opposed to storage arrays, it is not possible to resize memory arrays (e.g. the .push
member functions are not available).
You either have to calculate the required size in advance or create a new memory array and copy every element.
As all variables in Solidity, the elements of newly allocated arrays are always initialized with the default value.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
assert(a.length == 7);
assert(b.length == len);
a[6] = 8;
}
}
動的な長さを持つメモリ配列は、 new
演算子を用いて作成することができます。
ストレージ配列とは異なり,メモリ配列のサイズを変更することはできません。(例えば,メンバ関数 .push
は利用できません)
あらかじめ必要なサイズを計算しておくか、新しいメモリ配列を作成してすべての要素をコピーする必要があります。
Solidity のすべての変数と同様に、新しく割り当てられた配列の要素は、常にデフォルト値で初期化されます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
assert(a.length == 7);
assert(b.length == len);
a[6] = 8;
}
}
配列リテラル(Array Literals)
原文
An array literal is a comma-separated list of one or more expressions, enclosed in square brackets ([...]
).
For example [1, a, f(3)]
.
The type of the array literal is determined as follows:
It is always a statically-sized memory array whose length is the number of expressions.
The base type of the array is the type of the first expression on the list such that all other expressions can be implicitly converted to it. It is a type error if this is not possible.
It is not enough that there is a type all the elements can be converted to. One of the elements has to be of that type.
In the example below, the type of [1, 2, 3]
is uint8[3]
memory, because the type of each of these constants is uint8
.
If you want the result to be a uint[3] memory
type, you need to convert the first element to uint
.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] memory) public pure {
// ...
}
}
The array literal [1, -1]
is invalid because the type of the first expression is uint8
while the type of the second is int8
and they cannot be implicitly converted to each other.
To make it work, you can use [int8(1), -1]
, for example.
Since fixed-size memory arrays of different type cannot be converted into each other (even if the base types can), you always have to specify a common base type explicitly if you want to use two-dimensional array literals:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure returns (uint24[2][4] memory) {
uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
// The following does not work, because some of the inner arrays are not of the right type.
// uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
return x;
}
}
Fixed size memory arrays cannot be assigned to dynamically-sized memory arrays, i.e. the following is not possible:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
// This will not compile.
contract C {
function f() public {
// The next line creates a type error because uint[3] memory
// cannot be converted to uint[] memory.
uint[] memory x = [uint(1), 3, 4];
}
}
It is planned to remove this restriction in the future, but it creates some complications because of how arrays are passed in the ABI.
If you want to initialize dynamically-sized arrays, you have to assign the individual elements:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure {
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
}
}
配列リテラルは、1つ以上の式を角括弧 ([...]
) で囲み、カンマで区切ったリストです。
例えば、[1, a, f(3)]
のようなものです。
配列リテラルの型は以下のように決定されます。
常に静的なサイズのメモリ配列で、その長さは式の数である。
配列の基本型は,リストの最初の式の型であり,他のすべての式はこれに暗黙のうちに変換されます。
これができない場合は,型エラーとなります。
すべての要素が変換可能な型があるだけでは不十分です。
要素の1つがその型でなければならない。
以下の例では、それぞれの定数の型が uint8
であるため、 [1, 2, 3]
の型は uint8[3]
メモリとなります。
もし、結果を uint[3] memory
型にしたい場合は、最初の要素を uint
に変換する必要があります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] memory) public pure {
// ...
}
}
配列リテラル [1, -1]
は無効です。なぜなら、最初の式の型は uint8
であるのに対し、2番目の式の型は int8
で、互いに暗黙のうちに変換することができないからです。
これを動作させるには、例えば [int8(1), -1]
を使用します。
異なる型の固定サイズメモリ配列は(たとえ基本型が異なっていても)相互に変換できないので、2次元配列リテラルを使用する場合は、常に共通の基本型を明示的に指定する必要があります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure returns (uint24[2][4] memory) {
uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
// The following does not work, because some of the inner arrays are not of the right type.
// uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
return x;
}
}
固定サイズのメモリアレイは、動的サイズのメモリアレイに割り当てることはできません、すなわち、以下のことはできません。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
// This will not compile.
contract C {
function f() public {
// The next line creates a type error because uint[3] memory
// cannot be converted to uint[] memory.
uint[] memory x = [uint(1), 3, 4];
}
}
将来的にはこの制限を取り除く予定ですが、ABIでの配列の受け渡し方法の関係で、やや複雑な問題が生じます。
動的なサイズの配列を初期化したい場合は、個々の要素を代入する必要があります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure {
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
}
}
配列メンバ(Array Members)
原文
-
length:
Arrays have alength
member that contains their number of elements.
The length of memory arrays is fixed (but dynamic, i.e. it can depend on runtime parameters) once they are created. -
push():
Dynamic storage arrays andbytes
(notstring
) have a member function calledpush()
that you can use to append a zero-initialised element at the end of the array.
It returns a reference to the element, so that it can be used likex.push().t = 2
orx.push() = b
. -
push(x):
Dynamic storage arrays andbytes
(notstring
) have a member function calledpush(x)
that you can use to append a given element at the end of the array.
The function returns nothing. -
pop():
Dynamic storage arrays andbytes
(notstring
) have a member function calledpop()
that you can use to remove an element from the end of the array.
This also implicitly calls delete on the removed element.
The function returns nothing.
Note
Increasing the length of a storage array by calling push()
has constant gas costs because storage is zero-initialised, while decreasing the length by calling pop()
has a cost that depends on the “size” of the element being removed. If that element is an array, it can be very costly, because it includes explicitly clearing the removed elements similar to calling delete on them.
Note
To use arrays of arrays in external (instead of public) functions, you need to activate ABI coder v2.
Note
In EVM versions before Byzantium, it was not possible to access dynamic arrays return from function calls. If you call functions that return dynamic arrays, make sure to use an EVM that is set to Byzantium mode.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract ArrayContract {
uint[2**20] aLotOfIntegers;
// Note that the following is not a pair of dynamic arrays but a
// dynamic array of pairs (i.e. of fixed size arrays of length two).
// In Solidity, T[k] and T[] are always arrays with elements of type T,
// even if T itself is an array.
// Because of that, bool[2][] is a dynamic array of elements
// that are bool[2]. This is different from other languages, like C.
// Data location for all state variables is storage.
bool[2][] pairsOfFlags;
// newPairs is stored in memory - the only possibility
// for public contract function arguments
function setAllFlagPairs(bool[2][] memory newPairs) public {
// assignment to a storage array performs a copy of ``newPairs`` and
// replaces the complete array ``pairsOfFlags``.
pairsOfFlags = newPairs;
}
struct StructType {
uint[] contents;
uint moreInfo;
}
StructType s;
function f(uint[] memory c) public {
// stores a reference to ``s`` in ``g``
StructType storage g = s;
// also changes ``s.moreInfo``.
g.moreInfo = 2;
// assigns a copy because ``g.contents``
// is not a local variable, but a member of
// a local variable.
g.contents = c;
}
function setFlagPair(uint index, bool flagA, bool flagB) public {
// access to a non-existing index will throw an exception
pairsOfFlags[index][0] = flagA;
pairsOfFlags[index][1] = flagB;
}
function changeFlagArraySize(uint newSize) public {
// using push and pop is the only way to change the
// length of an array
if (newSize < pairsOfFlags.length) {
while (pairsOfFlags.length > newSize)
pairsOfFlags.pop();
} else if (newSize > pairsOfFlags.length) {
while (pairsOfFlags.length < newSize)
pairsOfFlags.push();
}
}
function clear() public {
// these clear the arrays completely
delete pairsOfFlags;
delete aLotOfIntegers;
// identical effect here
pairsOfFlags = new bool[2][](0);
}
bytes byteData;
function byteArrays(bytes memory data) public {
// byte arrays ("bytes") are different as they are stored without padding,
// but can be treated identical to "uint8[]"
byteData = data;
for (uint i = 0; i < 7; i++)
byteData.push();
byteData[3] = 0x08;
delete byteData[2];
}
function addFlag(bool[2] memory flag) public returns (uint) {
pairsOfFlags.push(flag);
return pairsOfFlags.length;
}
function createMemoryArray(uint size) public pure returns (bytes memory) {
// Dynamic memory arrays are created using `new`:
uint[2][] memory arrayOfPairs = new uint[2][](size);
// Inline arrays are always statically-sized and if you only
// use literals, you have to provide at least one type.
arrayOfPairs[0] = [uint(1), 2];
// Create a dynamic byte array:
bytes memory b = new bytes(200);
for (uint i = 0; i < b.length; i++)
b[i] = bytes1(uint8(i));
return b;
}
}
-
length:
配列は、その要素数を表すlength
メンバを持ちます。
メモリ配列の長さは、一度作成されると固定されます。(ただし、動的であり、ランタイムパラメータに依存する場合があります) -
push():
動的ストレージの配列とbytes
(string
ではない) にはpush()
というメンバ関数があり、これを使用すると、ゼロ初期化した要素を配列の末尾に追加することができます。
この関数は要素への参照を返すので、x.push().t = 2
やx.push() = b
のように使用することができます。 -
push(x):
動的ストレージの配列とbytes
(string
ではない) にはpush(x)
というメンバ関数があり、これを使うと配列の末尾に指定した要素を追加することができます。
この関数は何も返しません。 -
pop():
動的ストレージの配列とbytes
(string
ではない) にはpop()
というメンバ関数があり、これを使用して配列の末尾から要素を削除することができます。
これは、削除された要素に対して暗黙のうちに delete を呼び出すことにもなります。
この関数は何も返しません。
Note
storageはゼロ初期化されるため、push()
を呼び出してstorage配列の長さを増加させることは、一定のgasコストになります。
一方、 pop()
を呼び出して長さを減少させるには、削除される要素の "サイズ" に依存したコストが発生します。
その要素が配列の場合、削除された要素に対して delete を呼び出すのと同様に明示的にクリアすることが含まれるため、非常にコストがかかります。
Note
外部関数で配列の配列を(publicではなく)使用するには、ABIコーダーv2を有効にする必要があります。
Note
Byzantium以前のEVMバージョンでは、関数呼び出しから返される動的配列にアクセスすることは不可能でした。
動的配列を返す関数を呼び出す場合は、必ずByzantiumモードに設定されたEVMを使用してください。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract ArrayContract {
uint[2**20] aLotOfIntegers;
// Note that the following is not a pair of dynamic arrays but a
// dynamic array of pairs (i.e. of fixed size arrays of length two).
// In Solidity, T[k] and T[] are always arrays with elements of type T,
// even if T itself is an array.
// Because of that, bool[2][] is a dynamic array of elements
// that are bool[2]. This is different from other languages, like C.
// Data location for all state variables is storage.
bool[2][] pairsOfFlags;
// newPairs is stored in memory - the only possibility
// for public contract function arguments
function setAllFlagPairs(bool[2][] memory newPairs) public {
// assignment to a storage array performs a copy of ``newPairs`` and
// replaces the complete array ``pairsOfFlags``.
pairsOfFlags = newPairs;
}
struct StructType {
uint[] contents;
uint moreInfo;
}
StructType s;
function f(uint[] memory c) public {
// stores a reference to ``s`` in ``g``
StructType storage g = s;
// also changes ``s.moreInfo``.
g.moreInfo = 2;
// assigns a copy because ``g.contents``
// is not a local variable, but a member of
// a local variable.
g.contents = c;
}
function setFlagPair(uint index, bool flagA, bool flagB) public {
// access to a non-existing index will throw an exception
pairsOfFlags[index][0] = flagA;
pairsOfFlags[index][1] = flagB;
}
function changeFlagArraySize(uint newSize) public {
// using push and pop is the only way to change the
// length of an array
if (newSize < pairsOfFlags.length) {
while (pairsOfFlags.length > newSize)
pairsOfFlags.pop();
} else if (newSize > pairsOfFlags.length) {
while (pairsOfFlags.length < newSize)
pairsOfFlags.push();
}
}
function clear() public {
// these clear the arrays completely
delete pairsOfFlags;
delete aLotOfIntegers;
// identical effect here
pairsOfFlags = new bool[2][](0);
}
bytes byteData;
function byteArrays(bytes memory data) public {
// byte arrays ("bytes") are different as they are stored without padding,
// but can be treated identical to "uint8[]"
byteData = data;
for (uint i = 0; i < 7; i++)
byteData.push();
byteData[3] = 0x08;
delete byteData[2];
}
function addFlag(bool[2] memory flag) public returns (uint) {
pairsOfFlags.push(flag);
return pairsOfFlags.length;
}
function createMemoryArray(uint size) public pure returns (bytes memory) {
// Dynamic memory arrays are created using `new`:
uint[2][] memory arrayOfPairs = new uint[2][](size);
// Inline arrays are always statically-sized and if you only
// use literals, you have to provide at least one type.
arrayOfPairs[0] = [uint(1), 2];
// Create a dynamic byte array:
bytes memory b = new bytes(200);
for (uint i = 0; i < b.length; i++)
b[i] = bytes1(uint8(i));
return b;
}
}
ストレージ配列要素への懸垂参照(Dangling References to Storage Array Elements)
原文
When working with storage arrays, you need to take care to avoid dangling references.
A dangling reference is a reference that points to something that no longer exists or has been moved without updating the reference.
A dangling reference can for example occur, if you store a reference to an array element in a local variable and then .pop()
from the containing array:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
contract C {
uint[][] s;
function f() public {
// Stores a pointer to the last array element of s.
uint[] storage ptr = s[s.length - 1];
// Removes the last array element of s.
s.pop();
// Writes to the array element that is no longer within the array.
ptr.push(0x42);
// Adding a new element to ``s`` now will not add an empty array, but
// will result in an array of length 1 with ``0x42`` as element.
s.push();
assert(s[s.length - 1][0] == 0x42);
}
}
The write in ptr.push(0x42)
will not revert, despite the fact that ptr
no longer refers to a valid element of s
.
Since the compiler assumes that unused storage is always zeroed, a subsequent s.push()
will not explicitly write zeroes to storage, so the last element of s
after that push()
will have length 1
and contain 0x42
as its first element.
Note that Solidity does not allow to declare references to value types in storage.
These kinds of explicit dangling references are restricted to nested reference types.
However, dangling references can also occur temporarily when using complex expressions in tuple assignments:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
contract C {
uint[] s;
uint[] t;
constructor() {
// Push some initial values to the storage arrays.
s.push(0x07);
t.push(0x03);
}
function g() internal returns (uint[] storage) {
s.pop();
return t;
}
function f() public returns (uint[] memory) {
// The following will first evaluate ``s.push()`` to a reference to a new element
// at index 1. Afterwards, the call to ``g`` pops this new element, resulting in
// the left-most tuple element to become a dangling reference. The assignment still
// takes place and will write outside the data area of ``s``.
(s.push(), g()[0]) = (0x42, 0x17);
// A subsequent push to ``s`` will reveal the value written by the previous
// statement, i.e. the last element of ``s`` at the end of this function will have
// the value ``0x42``.
s.push();
return s;
}
}
It is always safer to only assign to storage once per statement and to avoid complex expressions on the left-hand-side of an assignment.
You need to take particular care when dealing with references to elements of bytes
arrays, since a .push()
on a bytes array may switch from short to long layout in storage.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
// This will report a warning
contract C {
bytes x = "012345678901234567890123456789";
function test() external returns(uint) {
(x.push(), x.push()) = (0x01, 0x02);
return x.length;
}
}
Here, when the first x.push()
is evaluated, x
is still stored in short layout, thereby x.push()
returns a reference to an element in the first storage slot of x
.
However, the second x.push()
switches the bytes array to large layout.
Now the element that x.push()
referred to is in the data area of the array while the reference still points at its original location, which is now a part of the length field and the assignment will effectively garble the length of x
.
To be safe, only enlarge bytes arrays by at most one element during a single assignment and do not simultaneously index-access the array in the same statement.
While the above describes the behaviour of dangling storage references in the current version of the compiler, any code with dangling references should be considered to have undefined behaviour.
In particular, this means that any future version of the compiler may change the behaviour of code that involves dangling references.
Be sure to avoid dangling references in your code!
ストレージ配列を扱う場合、懸垂参照を避けるように注意する必要があります。
懸垂参照とは、もう存在しないものを指している参照や、参照を更新せずに移動してしまったものを指します。
例えば、ローカル変数に配列の要素への参照を格納し、それを含む配列から .pop()
した場合、懸垂参照が発生する可能性があります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
contract C {
uint[][] s;
function f() public {
// Stores a pointer to the last array element of s.
uint[] storage ptr = s[s.length - 1];
// Removes the last array element of s.
s.pop();
// Writes to the array element that is no longer within the array.
ptr.push(0x42);
// Adding a new element to ``s`` now will not add an empty array, but
// will result in an array of length 1 with ``0x42`` as element.
s.push();
assert(s[s.length - 1][0] == 0x42);
}
}
ptr.push(0x42)
の書き込みは、ptr
がもはや s
の有効な要素を参照していないにもかかわらず、元に戻りません。
コンパイラは、未使用のストレージは常にゼロになると仮定しているので、その後の s.push()
は明示的にストレージにゼロを書き込まないので、 push()
の後の s
の最後の要素は、長さが 1
で、最初の要素として 0x42
を含むことになります。
Solidity では、ストレージ内の値型への参照を宣言することができないことに注意してください。
これらの種類の明示的なぶら下がり参照は、ネストされた参照タイプに制限されています。
しかし、タプル代入で複雑な式を使用する場合、懸垂参照は一時的に発生する可能性もあります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
contract C {
uint[] s;
uint[] t;
constructor() {
// Push some initial values to the storage arrays.
s.push(0x07);
t.push(0x03);
}
function g() internal returns (uint[] storage) {
s.pop();
return t;
}
function f() public returns (uint[] memory) {
// The following will first evaluate ``s.push()`` to a reference to a new element
// at index 1. Afterwards, the call to ``g`` pops this new element, resulting in
// the left-most tuple element to become a dangling reference. The assignment still
// takes place and will write outside the data area of ``s``.
(s.push(), g()[0]) = (0x42, 0x17);
// A subsequent push to ``s`` will reveal the value written by the previous
// statement, i.e. the last element of ``s`` at the end of this function will have
// the value ``0x42``.
s.push();
return s;
}
}
ストレージへの代入は1文につき1回とし、代入の左辺に複雑な式を置かないようにするのが常に無難です。
bytes 配列の要素への参照を扱うときには、特に注意が必要です。なぜなら、bytes 配列に対して .push()
を行うと、ストレージ内で short から long にレイアウトが切り替わる可能性があるからです。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
// This will report a warning
contract C {
bytes x = "012345678901234567890123456789";
function test() external returns(uint) {
(x.push(), x.push()) = (0x01, 0x02);
return x.length;
}
}
ここで、最初の x.push()
が評価されたとき、 x
はまだショートレイアウトで保存されています。したがって、 x.push()
は x
の最初のストレージスロットにある要素への参照を返します。
しかし、2回目の x.push()
で、バイト配列はラージレイアウトに切り替わります。
このとき、x.push()
が参照していた要素は配列のデータ領域にあり、参照は元の位置を指しています。これは現在では length フィールドの一部であり、この代入によって x
の長さは事実上文字化けしてしまいます。
安全のために、1回の代入でバイト配列を最大1要素だけ拡大し、同じ文の中で同時に配列にインデックスアクセスしないようにしてください。
上記は現在のバージョンのコンパイラにおける懸垂ストレージ参照の 動作を説明したものですが、懸垂参照を含むコードはすべて未定義の動作と見なす べきです。
特に、将来のバージョンのコンパイラでは、懸垂参照を含むコードの動作が変更される可能性があることを意味します。
コードに懸垂参照が含まれないように注意してください!
配列スライス(Array Slices)
原文
Array slices are a view on a contiguous portion of an array.
They are written as x[start:end]
, where start
and end
are expressions resulting in a uint256 type (or implicitly convertible to it).
The first element of the slice is x[start]
and the last element is x[end - 1]
.
If start
is greater than end
or if end
is greater than the length of the array, an exception is thrown.
Both start
and end
are optional: start
defaults to 0
and end
defaults to the length of the array.
Array slices do not have any members.
They are implicitly convertible to arrays of their underlying type and support index access.
Index access is not absolute in the underlying array, but relative to the start of the slice.
Array slices do not have a type name which means no variable can have an array slices as type, they only exist in intermediate expressions.
Note
As of now, array slices are only implemented for calldata arrays.
Array slices are useful to ABI-decode secondary data passed in function parameters:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.5 <0.9.0;
contract Proxy {
/// @dev Address of the client contract managed by proxy i.e., this contract
address client;
constructor(address client_) {
client = client_;
}
/// Forward call to "setOwner(address)" that is implemented by client
/// after doing basic validation on the address argument.
function forward(bytes calldata payload) external {
bytes4 sig = bytes4(payload[:4]);
// Due to truncating behaviour, bytes4(payload) performs identically.
// bytes4 sig = bytes4(payload);
if (sig == bytes4(keccak256("setOwner(address)"))) {
address owner = abi.decode(payload[4:], (address));
require(owner != address(0), "Address of owner cannot be zero.");
}
(bool status,) = client.delegatecall(payload);
require(status, "Forwarded call failed.");
}
}
配列スライスは、配列の連続した部分に対するビューです。
ここで、 start
と end
は uint256 型になる式です。(または、暗黙のうちに変換できます)
スライスの最初の要素は x[start]
であり,最後の要素は x[end - 1]
です。
start
が end
よりも大きい場合、あるいは end
が配列の長さよりも大きい場合は、例外がスローされる。
start
と end
はどちらも省略可能です。 start
のデフォルトは 0
で、 end
のデフォルトは配列の長さです。
配列スライスはメンバを持ちません。
これらは、その基礎となる型の配列に暗黙のうちに変換され、インデックスアクセスをサポートします。
インデックスアクセスは、基礎となる配列の絶対的なものではなく、スライスの開始点からの相対的なものです。
配列スライスは型名を持たないので、どの変数も配列スライスを型として持つことはできず、中間式にのみ存在します。
Note
現在のところ、配列スライスは calldata 配列に対してのみ実装されています。
配列スライスは、関数のパラメータで渡される2次データをABIデコードするのに便利です。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.5 <0.9.0;
contract Proxy {
/// @dev Address of the client contract managed by proxy i.e., this contract
address client;
constructor(address client_) {
client = client_;
}
/// Forward call to "setOwner(address)" that is implemented by client
/// after doing basic validation on the address argument.
function forward(bytes calldata payload) external {
bytes4 sig = bytes4(payload[:4]);
// Due to truncating behaviour, bytes4(payload) performs identically.
// bytes4 sig = bytes4(payload);
if (sig == bytes4(keccak256("setOwner(address)"))) {
address owner = abi.decode(payload[4:], (address));
require(owner != address(0), "Address of owner cannot be zero.");
}
(bool status,) = client.delegatecall(payload);
require(status, "Forwarded call failed.");
}
}
構造体(Structs)
原文
Solidity provides a way to define new types in the form of structs, which is shown in the following example:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// Defines a new type with two fields.
// Declaring a struct outside of a contract allows
// it to be shared by multiple contracts.
// Here, this is not really needed.
struct Funder {
address addr;
uint amount;
}
contract CrowdFunding {
// Structs can also be defined inside contracts, which makes them
// visible only there and in derived contracts.
struct Campaign {
address payable beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}
uint numCampaigns;
mapping (uint => Campaign) campaigns;
function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
campaignID = numCampaigns++; // campaignID is return variable
// We cannot use "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
// because the right hand side creates a memory-struct "Campaign" that contains a mapping.
Campaign storage c = campaigns[campaignID];
c.beneficiary = beneficiary;
c.fundingGoal = goal;
}
function contribute(uint campaignID) public payable {
Campaign storage c = campaigns[campaignID];
// Creates a new temporary memory struct, initialised with the given values
// and copies it over to storage.
// Note that you can also use Funder(msg.sender, msg.value) to initialise.
c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
c.amount += msg.value;
}
function checkGoalReached(uint campaignID) public returns (bool reached) {
Campaign storage c = campaigns[campaignID];
if (c.amount < c.fundingGoal)
return false;
uint amount = c.amount;
c.amount = 0;
c.beneficiary.transfer(amount);
return true;
}
}
The contract does not provide the full functionality of a crowdfunding contract, but it contains the basic concepts necessary to understand structs.
Struct types can be used inside mappings and arrays and they can themselves contain mappings and arrays.
It is not possible for a struct to contain a member of its own type, although the struct itself can be the value type of a mapping member or it can contain a dynamically-sized array of its type.
This restriction is necessary, as the size of the struct has to be finite.
Note how in all the functions, a struct type is assigned to a local variable with data location storage
.
This does not copy the struct but only stores a reference so that assignments to members of the local variable actually write to the state.
Of course, you can also directly access the members of the struct without assigning it to a local variable, as in campaigns[campaignID].amount = 0
.
Note
Until Solidity 0.7.0, memory-structs containing members of storage-only types (e.g. mappings) were allowed and assignments like campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)
in the example above would work and just silently skip those members.
Solidity では、次の例に示すように、構造体の形で新しい型を定義する方法が用意されています。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// Defines a new type with two fields.
// Declaring a struct outside of a contract allows
// it to be shared by multiple contracts.
// Here, this is not really needed.
struct Funder {
address addr;
uint amount;
}
contract CrowdFunding {
// Structs can also be defined inside contracts, which makes them
// visible only there and in derived contracts.
struct Campaign {
address payable beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}
uint numCampaigns;
mapping (uint => Campaign) campaigns;
function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
campaignID = numCampaigns++; // campaignID is return variable
// We cannot use "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
// because the right hand side creates a memory-struct "Campaign" that contains a mapping.
Campaign storage c = campaigns[campaignID];
c.beneficiary = beneficiary;
c.fundingGoal = goal;
}
function contribute(uint campaignID) public payable {
Campaign storage c = campaigns[campaignID];
// Creates a new temporary memory struct, initialised with the given values
// and copies it over to storage.
// Note that you can also use Funder(msg.sender, msg.value) to initialise.
c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
c.amount += msg.value;
}
function checkGoalReached(uint campaignID) public returns (bool reached) {
Campaign storage c = campaigns[campaignID];
if (c.amount < c.fundingGoal)
return false;
uint amount = c.amount;
c.amount = 0;
c.beneficiary.transfer(amount);
return true;
}
}
このコントラクトは、クラウドファンディングの契約の全機能を提供するものではありませんが、構造体を理解するために必要な基本的な概念は含まれています。
構造体はマッピングや配列の内部で使用することができ、構造体自身がマッピングや配列を含むことができます。
構造体はそれ自身の型のメンバを含むことはできませんが、構造体自身がマッピングメンバの値型になることや、その型の動的サイズの配列を含むことはできます。
構造体のサイズは有限でなければならないので、この制限は必要です。
すべての関数で、構造体の型がデータの場所 storage
を持つローカル変数に代入されていることに注目してください。
これは構造体をコピーするのではなく、ローカル変数のメンバへの代入が実際に状態に書き込まれるように、参照を格納するだけです。
もちろん、campaigns[campaignID].amount = 0
のように、ローカル変数に代入せずに構造体のメンバに直接アクセスすることも可能です。
Note
Solidity 0.7.0 までは、ストレージのみの型 (マッピングなど) のメンバを含むメモリ構造体が許可されており、上記の例の campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)
のように代入しても、これらのメンバを黙ってスキップするだけでした。
さいごに
ここでは、配列に注目しました。
配列の要素への参照を扱うときには、特に注意が必要です。
その4へ