타입

타입 (Types)

Solidity는 정적 타입 언어라서 각 변수(상태 변수와 지역 변수)의 타입을 지정해야 해요. Solidity는 여러 기본(elementary) 타입을 제공하며, 이들을 결합해 복합 타입을 만들 수 있어요. 추가로 타입은 연산자를 포함한 표현식 안에서 서로 상호작용할 수 있어요. 다양한 연산자의 빠른 참조는 연산자 우선순위를 보세요. Solidity에는 "undefined"나 "null" 값의 개념이 없지만, 새로 선언된 변수는 항상 타입에 의존하는 기본값을 가져요. 예상치 못한 값을 처리하려면 revert 함수를 사용해 전체 트랜잭션을 되돌리거나, 성공 여부를 나타내는 두 번째 bool 값과 함께 튜플을 반환해야 해요.

출처: 문서

본문

값 타입 (Value Types)

다음 타입들은 그 변수가 항상 값으로 전달되기 때문에 값 타입이라고 불러요. 즉 함수 인자나 할당에서 사용될 때 항상 복사돼요. 참조 타입과 달리 값 타입 선언은 데이터 위치를 지정하지 않아요. 스택에 저장하기에 충분히 작기 때문이에요. 유일한 예외는 상태 변수예요. 상태 변수는 기본적으로 스토리지에 위치하지만, transient, constant, immutable로 표시할 수도 있어요.

불리언 (Booleans)

bool : 가능한 값은 상수 true와 false예요.

연산자:

  • ! (논리 부정)
  • && (논리곱, "and")
  • || (논리합, "or")
  • == (등호)
  • != (부등호)

||과 && 연산자는 일반적인 단락 평가(short-circuiting) 규칙을 적용해요. 즉 표현식 f(x) || g(y)에서 f(x)가 true로 평가되면 g(y)는 부작용이 있을 수 있어도 평가되지 않아요.

정수 (Integers)

int / uint : 다양한 크기의 부호 있는/부호 없는 정수. uint8부터 uint256까지 8 단위로 (8 비트부터 256 비트까지의 부호 없는) 키워드와 int8부터 int256까지가 있어요. uint와 int는 각각 uint256과 int256의 별칭이에요.

연산자:

  • 비교: <=, <, ==, !=, >=, > (bool로 평가)
  • 비트 연산자: &, |, ^ (배타적 논리합), ~ (비트 부정)
  • 시프트 연산자: << (왼쪽 시프트), >> (오른쪽 시프트)
  • 산술 연산자: +, -, 단항 - (부호 있는 정수만), *, /, % (모듈로), ** (지수)

정수 타입 X에 대해 type(X).min과 type(X).max를 사용해 타입이 표현할 수 있는 최소값과 최대값에 접근할 수 있어요.

경고: Solidity의 정수는 특정 범위로 제한돼요. 예를 들어 uint32의 경우 0부터 2**32 - 1까지예요. 이러한 타입에서 산술이 수행되는 두 가지 모드가 있어요: "wrapping" 또는 "unchecked" 모드와 "checked" 모드. 기본적으로 산술은 항상 "checked"예요. 즉 연산 결과가 타입의 값 범위를 벗어나면 실패한 어설션을 통해 호출이 되돌아가요. unchecked { ... }를 사용해 "unchecked" 모드로 전환할 수 있어요. 자세한 내용은 unchecked 섹션에서 확인할 수 있어요.

비교 (Comparisons)

비교 결과 값은 정수 값을 비교해 얻어요.

비트 연산 (Bit operations)

비트 연산은 숫자의 2의 보수 표현에 대해 수행돼요. 즉 예를 들어 ~int256(0) == int256(-1)이에요.

시프트 (Shifts)

시프트 연산의 결과는 왼쪽 피연산자의 타입을 가지며, 결과를 그 타입에 맞게 잘라내요. 오른쪽 피연산자는 부호 없는 타입이어야 하고, 부호 있는 타입으로 시프트하려 하면 컴파일 오류가 발생해요. 시프트는 다음과 같은 방식으로 2의 거듭제곱 곱셈으로 "시뮬레이션"할 수 있어요. 왼쪽 피연산자의 타입으로의 잘림은 항상 마지막에 수행되지만 명시적으로 언급되지는 않아요.

  • x << y는 수학적 표현 x * 2**y와 동등해요.
  • x >> y는 음의 무한대 방향으로 반올림한 수학적 표현 x / 2**y와 동등해요.

경고: 버전 0.5.0 이전에는 음수 x에 대한 오른쪽 시프트 x >> y가 0 방향으로 반올림한 수학적 표현 x / 2**y와 동등했어요. 즉 오른쪽 시프트는 음의 무한대 방향(내림) 대신 0 방향(올림) 반올림을 사용했어요.

참고: 시프트 연산에는 산술 연산처럼 오버플로우 검사가 절대 수행되지 않아요. 대신 결과가 항상 잘려요.

덧셈, 뺄셈, 곱셈 (Addition, Subtraction and Multiplication)

덧셈, 뺄셈, 곱셈은 일반적인 의미를 가지며, 오버플로우와 언더플로우에 관한 두 가지 다른 모드가 있어요. 기본적으로 모든 산술은 언더/오버플로우가 검사되지만, unchecked 블록을 사용해 비활성화할 수 있고, 그러면 wrapping 산술이 돼요. 자세한 내용은 해당 섹션에서 확인할 수 있어요.

표현식 -x는 (T(0) - x)와 동등한데, 여기서 T는 x의 타입이에요. 부호 있는 타입에만 적용할 수 있어요. x가 음수이면 -x의 값은 양수일 수 있어요. 2의 보수 표현에서 비롯된 또 다른 주의사항이 있어요: int x = type(int).min;이면 -x는 양의 범위에 맞지 않아요. 즉 unchecked { assert(-x == x); }가 작동하고, checked 모드에서 사용될 때 표현식 -x는 실패한 어설션을 초래해요.

나눗셈 (Division)

연산 결과의 타입은 항상 피연산자 중 하나의 타입이므로, 정수에 대한 나눗셈은 항상 정수를 결과로 내요. Solidity에서 나눗셈은 0을 향해 반올림해요. 즉 int256(-5) / int256(2) == int256(-2)이에요. 대조적으로 리터럴에 대한 나눗셈은 임의 정밀도의 분수 값을 낳는다는 점을 참고하세요.

참고: 0으로 나누면 Panic 에러가 발생해요. 이 검사는 unchecked { ... }로 비활성화할 수 없어요. 참고: 표현식 type(int).min / (-1)은 나눗셈이 오버플로우를 일으키는 유일한 경우예요. checked 산술 모드에서는 실패한 어설션을 일으키고, wrapping 모드에서는 값이 type(int).min이에요.

모듈로 (Modulo)

모듈로 연산 a % n은 피연산자 a를 피연산자 n으로 나눈 후 나머지 r을 산출하는데, 여기서 q = int(a / n)이고 r = a - (n * q)이에요. 즉 모듈로 결과는 왼쪽 피연산자와 같은 부호(또는 0)를 가지며, 음수 a에 대해 a % n == -(-a % n)가 성립해요:

  • int256(5) % int256(2) == int256(1)
  • int256(5) % int256(-2) == int256(1)
  • int256(-5) % int256(2) == int256(-1)
  • int256(-5) % int256(-2) == int256(-1)

참고: 0으로 모듈로를 하면 Panic 에러가 발생해요. 이 검사는 unchecked { ... }로 비활성화할 수 없어요.

지수 (Exponentiation)

지수 연산은 지수에 부호 없는 타입에서만 사용할 수 있어요. 지수 연산의 결과 타입은 항상 밑의 타입과 같아요. 결과를 담기에 충분히 큰지 주의하고 잠재적 어설션 실패나 wrapping 동작을 대비하세요.

참고: checked 모드에서 지수 연산은 작은 밑에 대해서만 비교적 저렴한 exp opcode를 사용해요. x**3의 경우 표현식 x*x*x가 더 저렴할 수 있어요. 어느 경우든 가스 비용 테스트와 최적화 프로그램 사용이 권장돼요. 참고: 0**0은 EVM에 의해 1로 정의된다는 점을 참고하세요.

고정소수점 수 (Fixed Point Numbers)

경고: 고정소수점 수는 Solidity에서 아직 완전히 지원되지 않아요. 선언할 수는 있지만, 할당하거나 할당받을 수는 없어요.

fixed / ufixed : 다양한 크기의 부호 있는/부호 없는 고정소수점 수. 키워드 ufixedMxN과 fixedMxN. 여기서 M은 타입이 차지하는 비트 수를, N은 사용 가능한 소수 자릿수를 나타내요. M은 8로 나눠져야 하고 8에서 256 비트까지 나아가요. N은 0과 80 사이(포함)여야 해요. ufixed와 fixed는 각각 ufixed128x18과 fixed128x18의 별칭이에요.

연산자:

  • 비교: <=, <, ==, !=, >=, > (bool로 평가)
  • 산술 연산자: +, -, 단항 -, *, /, % (모듈로)

참고: 부동소수점(많은 언어의 float와 double, 더 정확히는 IEEE 754 숫자)과 고정소수점 수의 주요 차이는 전자에서는 정수 부분과 분수 부분(소수점 뒤 부분)에 사용되는 비트 수가 유연한 반면, 후자에서는 엄격히 정의된다는 것이에요. 일반적으로 부동소수점에서는 거의 전체 공간이 숫자를 표현하는 데 사용되고, 소수점이 어디 있는지를 정의하는 비트는 소수에 불과해요.

주소 (Address)

주소 타입은 대체로 동일한 두 가지 형태가 있어요:

  • address : 20바이트 값(이더리움 주소 크기)을 담아요.
  • address payable : address와 같지만 transfer와 send 멤버가 추가로 있어요.

이 구분의 배경은 address payable이 Ether를 보낼 수 있는 주소인 반면, 일반 address에는 Ether를 보내면 안 된다는 것이에요. 예를 들어 그 주소가 Ether를 받도록 만들어지지 않은 스마트 컨트랙트일 수 있기 때문이에요.

타입 변환:

  • address payable에서 address로의 암시적 변환은 허용되지만, address에서 address payable로의 변환은 payable(<address>)로 명시적이어야 해요.
  • uint160, 정수 리터럴, bytes20, 컨트랙트 타입에 대해 address로/로부터의 명시적 변환이 허용돼요.
  • address 타입과 컨트랙트 타입의 표현식만 payable(...) 명시적 변환으로 address payable 타입으로 변환할 수 있어요. 컨트랙트 타입의 경우, 컨트랙트가 Ether를 받을 수 있어야만(즉 컨트랙트에 receive 또는 payable fallback 함수가 있어야만) 이 변환이 허용돼요. payable(0)은 유효하고 이 규칙의 예외라는 점을 참고하세요.

참고: address 타입의 변수가 필요하고 그곳으로 Ether를 보낼 계획이라면 이 요구사항을 보이도록 타입을 address payable로 선언하세요. 또한 이 구분이나 변환을 가능한 한 일찍 하세요. address와 address payable의 구분은 버전 0.5.0에서 도입됐어요. 또한 그 버전부터 컨트랙트는 address 타입으로 암시적으로 변환되지 않지만, receive 또는 payable fallback 함수가 있으면 명시적으로 address나 address payable로 변환할 수 있어요.

연산자:

  • <=, <, ==, !=, >=, >

경고: bytes32처럼 더 큰 바이트 크기를 사용하는 타입을 address로 변환하면 주소가 잘려요. 변환 모호성을 줄이기 위해 0.4.24 버전부터 컴파일러가 변환에서 잘림을 명시적으로 하도록 강제해요. 예를 들어 32바이트 값 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC를 생각해봐요. address(bytes20(b))를 사용하면 0x111122223333444455556666777788889999aAaa가 되고, address(uint160(uint256(b)))를 사용하면 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc가 돼요.

참고: EIP-55를 준수하는 혼합 대소문자 16진수는 자동으로 address 타입의 리터럴로 취급돼요. 주소 리터럴을 보세요.

주소의 멤버 (Members of Addresses)

모든 주소 멤버의 빠른 참조는 주소 타입의 멤버를 보세요.

  • balance와 transfer balance 속성을 사용해 주소의 잔액을 조회하고, transfer 함수를 사용해 (wei 단위로) payable 주소에 Ether를 보낼 수 있어요:
address payable x = payable(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10)
    x.transfer(10);

transfer 함수는 현재 컨트랙트의 잔액이 충분하지 않거나 수신 계정이 Ether 전송을 거부하면 실패해요. transfer 함수는 실패 시 되돌아가요.

참고: x가 컨트랙트 주소라면, 그 코드(더 정확히는 있는 경우 그 Receive Ether 함수, 아니면 있는 경우 그 Fallback 함수)가 transfer 호출과 함께 실행돼요(이것은 EVM의 특징이며 막을 수 없어요). 그 실행이 가스가 부족하거나 어떤 식으로든 실패하면 Ether 전송이 되돌아가고 현재 컨트랙트가 예외로 중단돼요. 경고: transfer는 더 이상 사용되지 않고(deprecated) 제거될 예정이에요. 단순한 Ether 전송은 여전히 선택적으로 최대 가스량을 제공하고 빈 페이로드를 가진 call 함수, 즉 call{value: <amount>}("")로 수행할 수 있어요. 기본적으로 이것은 일부 EVM 버전이 부과하는 추가 제한(tangerineWhistle이 도입한 63/64 규칙 같은)에 따라 남은 가스를 모두 전달해요. 어떤 외부 호출과 마찬가지로 gas 호출 옵션을 사용해 하한을 설정할 수 있어요. 명시적으로 한도를 스티펜드 값(2300 가스)으로 설정해 기능을 재현하는 것이 가능하지만, opcode 비용의 변화로 이 값은 더 이상 원래 의미를 갖지 않아요. 재진입에 대비해 다른 수단을 사용하는 것이 권장돼요.

  • send send는 transfer의 저수준 대응물이에요. 실행이 실패하면 현재 컨트랙트가 예외로 중단되지 않고 send가 false를 반환해요.

경고: send 사용에는 몇 가지 위험이 있어요: 호출 스택 깊이가 1024에 있으면 전송이 실패하고(항상 호출자가 강제할 수 있음), 수신자가 가스가 부족해도 실패해요. 따라서 안전한 Ether 전송을 위해 항상 send의 반환 값을 확인하고, transfer를 사용하거나, 더 나은 방법으로 수신자가 Ether를 출금하는 패턴을 사용하세요. 경고: send는 더 이상 사용되지 않고 제거될 예정이에요. 단순한 Ether 전송은 여전히 선택적으로 최대 가스량을 제공하고 빈 페이로드를 가진 call 함수, 즉 call{value: <amount>}("")로 수행할 수 있어요. 기본적으로 이것은 일부 EVM 버전이 부과하는 추가 제한(63/64 규칙 같은)에 따라 남은 가스를 모두 전달해요. 어떤 외부 호출과 마찬가지로 gas 호출 옵션을 사용해 하한을 설정할 수 있어요. 명시적으로 한도를 스티펜드 값(2300 가스)으로 설정해 기능을 재현하는 것이 가능하지만, opcode 비용의 변화로 이 값은 더 이상 원래 의미를 갖지 않아요. 재진입에 대비해 다른 수단을 사용하는 것이 권장돼요.

  • call, delegatecall, staticcall ABI를 따르지 않는 컨트랙트와 인터페이스를 하거나, 인코딩을 더 직접적으로 제어하려면 call, delegatecall, staticcall 함수가 제공돼요. 모두 단일 bytes memory 파라미터를 받고 성공 조건(bool)과 반환 데이터(bytes memory)를 반환해요. abi.encode, abi.encodePacked, abi.encodeWithSelector, abi.encodeWithSignature 함수를 사용해 구조화된 데이터를 인코딩할 수 있어요. 예:
bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);

경고: 이 함수들은 모두 저수준 함수이며 주의해서 사용해야 해요. 특히 알 수 없는 컨트랙트는 악의적일 수 있고, 호출하면 제어를 그 컨트랙트에 넘겨주게 되어 다시 당신의 컨트랙트로 콜백할 수 있으므로, 호출이 반환될 때 상태 변수의 변경에 대비하세요. 다른 컨트랙트와 상호작용하는 일반적인 방법은 컨트랙트 객체의 함수를 호출하는 것(x.f())이에요. 참고: 이전 버전의 Solidity는 이 함수들이 임의의 인자를 받을 수 있었고 bytes4 타입의 첫 인자를 다르게 처리했어요. 이러한 경계 사례는 버전 0.5.0에서 제거됐어요.

gas 수정자로 제공된 가스를 조정할 수 있어요:

address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName"));

마찬가지로 제공된 Ether 값도 제어할 수 있어요:

address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

마지막으로 이 수정자들을 결합할 수 있어요. 순서는 중요하지 않아요:

address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

비슷한 방식으로 delegatecall 함수를 사용할 수 있어요. 차이점은 주어진 주소의 코드만 사용되고 다른 모든 측면(스토리지, 잔액, …)은 현재 컨트랙트에서 가져온다는 것이에요. delegatecall의 목적은 다른 컨트랙트에 저장된 라이브러리 코드를 사용하는 것이에요. 사용자는 두 컨트랙트의 스토리지 배치가 delegatecall을 사용하기에 적합한지 보장해야 해요.

참고: homestead 이전에는 원래 msg.sender와 msg.value 값에 접근하지 못하는 callcode라는 제한된 변형만 사용할 수 있었어요. 이 함수는 버전 0.5.0에서 제거됐어요. byzantium 이후로는 staticcall도 사용할 수 있어요. 이것은 기본적으로 call과 같지만, 호출된 함수가 상태를 어떤 식으로든 수정하면 되돌아가요. call, delegatecall, staticcall 세 함수 모두 매우 저수준 함수이며 Solidity의 타입 안전성을 깨뜨리므로 최후의 수단으로만 사용해야 해요. gas 옵션은 세 메서드 모두에서 사용할 수 있고, value 옵션은 call에서만 사용할 수 있어요. 참고: 상태를 읽든 쓰든 하드코딩된 가스 값에 의존하는 것은 많은 함정이 있으므로 스마트 컨트랙트 코드에서 피하는 것이 좋아요. 또한 가스 접근은 미래에 바뀔 수 있어요.

  • code와 codehash 어떤 스마트 컨트랙트든 배포된 코드를 조회할 수 있어요. .code를 사용해 (비어 있을 수 있는) bytes memory로 EVM 바이트코드를 얻어요. .codehash를 사용해 그 코드의 Keccak-256 해시(bytes32)를 얻어요. addr.codehash가 keccak256(addr.code)를 사용하는 것보다 저렴하다는 점을 참고하세요.

경고: addr와 연관된 계정이 비어 있거나 존재하지 않으면(즉 EIP-161이 정의한 대로 코드가 없고 잔액이 0이며 nonce가 0이면) addr.codehash의 출력이 0일 수 있어요. 계정에 코드는 없지만 0이 아닌 잔액이나 nonce가 있으면 addr.codehash는 빈 데이터의 Keccak-256 해시(즉 keccak256("")이며 c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470와 같음)를 출력해요. EIP-1052가 정의한 대로요.

참고: 모든 컨트랙트는 address 타입으로 변환될 수 있으므로 address(this).balance를 사용해 현재 컨트랙트의 잔액을 조회할 수 있어요.

컨트랙트 타입 (Contract Types)

모든 컨트랙트는 자체 타입을 정의해요. 컨트랙트를 상속받는 컨트랙트로 암시적으로 변환할 수 있어요. 컨트랙트는 address 타입과 그 반대로 명시적으로 변환될 수 있어요. address payable 타입으로의/로부터의 명시적 변환은 컨트랙트 타입에 receive 또는 payable fallback 함수가 있을 때만 가능해요. 변환은 여전히 address(x)로 수행돼요. 컨트랙트 타입에 receive나 payable fallback 함수가 없으면 payable(address(x))로 address payable로의 변환을 할 수 있어요. 주소 타입 섹션에서 더 자세한 정보를 찾을 수 있어요.

참고: 0.5.0 버전 이전에는 컨트랙트가 address 타입에서 직접 파생됐고 address와 address payable 사이에 구분이 없었어요.

컨트랙트 타입의 지역 변수(MyContract c)를 선언하면 그 컨트랙트에서 함수를 호출할 수 있어요. 같은 컨트랙트 타입인 어딘가에서 할당하도록 주의하세요. 컨트랙트를 인스턴스화할 수도 있어요(즉 새로 생성된다는 뜻). 'new를 통한 컨트랙트' 섹션에서 자세한 내용을 찾을 수 있어요. 컨트랙트의 데이터 표현은 address 타입과 동일하며 이 타입은 ABI에서도 사용돼요. 컨트랙트는 어떤 연산자도 지원하지 않아요. 컨트랙트 타입의 멤버는 컨트랙트의 외부 함수와 public으로 표시된 상태 변수들이에요. 컨트랙트 C에 대해 type(C)를 사용해 컨트랙트에 대한 타입 정보에 접근할 수 있어요.

고정 크기 바이트 배열 (Fixed-size byte arrays)

값 타입 bytes1, bytes2, bytes3, …, bytes32는 1개에서 최대 32개의 바이트 시퀀스를 담아요.

연산자:

  • 비교: <=, <, ==, !=, >=, > (bool로 평가)
  • 비트 연산자: &, |, ^ (배타적 논리합), ~ (비트 부정)
  • 시프트 연산자: << (왼쪽 시프트), >> (오른쪽 시프트)
  • 인덱스 접근: x가 bytesI 타입이면 0 <= k < I에 대해 x[k]는 k번째 바이트를 (읽기 전용으로) 반환해요.

시프트 연산자는 오른쪽 피연산자로 부호 없는 정수 타입과 함께 작동하며(하지만 왼쪽 피연산자의 타입을 반환), 시프트할 비트 수를 나타내요. 부호 있는 타입으로 시프트하면 컴파일 오류가 발생해요.

멤버:

  • .length 는 바이트 배열의 고정 길이를 (읽기 전용으로) 산출해요.

참고: bytes1[] 타입은 바이트의 배열이지만, 패딩 규칙 때문에 각 요소에 대해 31바이트의 공간을 낭비해요(스토리지 제외). 대신 bytes 타입을 사용하는 것이 더 좋아요. 참고: 0.8.0 버전 이전에는 byte가 bytes1의 별칭이었어요.

주소 리터럴 (Address Literals)

주소 체크섬 테스트를 통과하는 16진수 리터럴, 예를 들어 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF은 address 타입이에요. 39~41자리 사이의 길이를 가진 16진수 리터럴 중 체크섬 테스트를 통과하지 못하는 것은 오류를 생성해요. (정수 타입의 경우) 앞에 또는 (bytesNN 타입의 경우) 뒤에 0을 붙여 오류를 제거할 수 있어요.

참고: 혼합 대소문자 주소 체크섬 형식은 EIP-55에 정의돼 있어요.

유리수와 정수 리터럴 (Rational and Integer Literals)

정수 리터럴은 0-9 범위의 숫자 시퀀스로 형성돼요. 십진수로 해석돼요. 예를 들어 69는 예순아홉을 의미해요. Solidity에는 8진수 리터럴이 없고 선행 0은 유효하지 않아요. 10진 분수 리터럴은 소수점 뒤에 적어도 하나의 숫자가 있는 .로 형성돼요. 예로는 .1과 1.3이 있어요(1.은 아님). 2e10 형태의 과학적 표기법도 지원되는데, 가수는 분수일 수 있지만 지수는 정수여야 해요. 리터럴 MeE는 M * 10**E와 동등해요. 예로는 2e10, -2e10, 2e-10, 2.5e1이 있어요.

숫자 리터럴의 자릿수를 구분해 가독성을 돕기 위해 밑줄을 사용할 수 있어요. 예를 들어 십진 123_000, 16진 0x2eff_abde, 과학적 십진 표기법 1_2e345_678이 모두 유효해요. 밑줄은 두 자릿수 사이에서만 허용되고 연속 밑줄은 하나만 허용돼요. 밑줄을 포함한 숫자 리터럴에 추가적인 의미가 부여되지는 않으며, 밑줄은 무시돼요.

숫자 리터럴 표현식은 비리터럴 타입으로 변환되기 전까지(즉 숫자 리터럴 표현식이 아닌 다른 것, 예: 불리언 리터럴과 함께 사용하거나 명시적 변환에 의해) 임의 정밀도를 유지해요. 이는 숫자 리터럴 표현식에서 계산이 오버플로우되지 않고 나눗셈이 잘리지 않는다는 것을 의미해요. 예를 들어 (2**800 + 1) - 2**800은 중간 결과가 기계 단어 크기에도 맞지 않음에도 상수 1(uint8 타입)을 결과로 내요. 게다가 .5 * 8은 정수 4를 결과로 내요(중간에 정수가 아닌 것을 사용했음에도).

경고: 대부분의 연산자는 리터럴에 적용될 때 리터럴 표현식을 생성하지만, 이 패턴을 따르지 않는 특정 연산자가 있어요:

  • 삼항 연산자 (... ? ... : ...),
  • 배열 첨자 (<array>[<index>]). 255 + (true ? 1 : 0)이나 255 + [1, 2, 3][0] 같은 표현식이 리터럴 256을 직접 사용하는 것과 동등할 것이라고 예상할 수 있지만, 실제로는 uint8 타입 안에서 계산되어 오버플로우할 수 있어요.

정수에 적용할 수 있는 연산자는 피연산자가 정수인 한 숫자 리터럴 표현식에도 적용할 수 있어요. 둘 중 하나가 분수이면 비트 연산은 허용되지 않고, 지수가 분수이면 지수 연산은 허용되지 않아요(비유리수가 될 수 있기 때문).

왼쪽(또는 밑) 피연산자로 리터럴 숫자와 오른쪽(지수) 피연산자로 정수 타입을 가진 시프트와 지수 연산은 오른쪽(지수) 피연산자의 타입과 무관하게 항상 uint256(음이 아닌 리터럴) 또는 int256(음수 리터럴) 타입으로 수행돼요.

경고: 정수 리터럴에 대한 나눗셈은 0.4.0 버전 이전에는 잘렸지만, 이제는 유리수로 변환돼요. 즉 5 / 2는 2가 아니라 2.5와 같아요.

참고: Solidity는 각 유리수에 대한 숫자 리터럴 타입이 있어요. 정수 리터럴과 유리수 리터럴은 숫자 리터럴 타입에 속해요. 게다가 모든 숫자 리터럴 표현식(즉 숫자 리터럴과 연산자만을 포함하는 표현식)은 숫자 리터럴 타입에 속해요. 그래서 숫자 리터럴 표현식 1 + 2와 2 + 1은 모두 유리수 3에 대한 같은 숫자 리터럴 타입에 속해요.

참고: 숫자 리터럴 표현식은 비리터럴 표현식과 함께 사용되는 즉시 비리터럴 타입으로 변환돼요. 타입을 무시하면 아래 b에 할당된 표현식의 값은 정수로 평가돼요. a가 uint128 타입이므로 표현식 2.5 + a는 적절한 타입을 가져야 해요. 2.5의 타입과 uint128의 공통 타입이 없으므로 Solidity 컴파일러는 이 코드를 받아들이지 않아요.

uint128 a = 1;
uint128 b = 2.5 + a + 0.5;

문자열 리터럴과 타입 (String Literals and Types)

문자열 리터럴은 이중 또는 단일 따옴표("foo" 또는 'bar')로 작성되고, 여러 연속 부분으로 나눌 수도 있어요("foo" "bar"는 "foobar"와 동등) — 이는 긴 문자열을 다룰 때 유용할 수 있어요. C처럼 끝에 0을 암시하지 않아요. "foo"는 4가 아닌 3바이트를 나타내요. 정수 리터럴과 마찬가지로 타입이 다양할 수 있지만, 들어맞으면 bytes1, …, bytes32, bytes, string으로 암시적으로 변환될 수 있어요. 예를 들어 bytes32 samevar = "stringliteral"에서는 문자열 리터럴이 bytes32 타입에 할당될 때 원시 바이트 형태로 해석돼요.

문자열 리터럴은 인쇄 가능한 ASCII 문자만 포함할 수 있어요. 즉 0x20 .. 0x7E 사이의 문자(경계 포함)예요. 추가로 문자열 리터럴은 다음 이스케이프 문자를 지원해요:

  • \<newline> (실제 줄바꿈 이스케이프)
  • \\ (백슬래시)
  • \' (단일 따옴표)
  • \" (이중 따옴표)
  • \n (줄바꿈)
  • \r (캐리지 리턴)
  • \t (탭)
  • \xNN (16진 이스케이프, 아래 참고)
  • \uNNNN (유니코드 이스케이프, 아래 참고)

\xNN은 16진 값을 받아 적절한 바이트를 삽입하고, \uNNNN은 유니코드 코드포인트를 받아 UTF-8 시퀀스를 삽입해요.

참고: 0.8.0 버전까지 세 가지 추가 이스케이프 시퀀스가 있었어요: \b, \f, \v. 그것들은 다른 언어에서 흔히 사용 가능하지만 실제로는 거의 필요하지 않아요. 필요하다면 다른 ASCII 문자와 마찬가지로 16진 이스케이프(각각 \x08, \x0c, \x0b)로 삽입할 수 있어요.

다음 예의 문자열은 길이가 10바이트예요. 줄바꿈 바이트로 시작하고, 그 뒤에 이중 따옴표, 단일 따옴표, 백슬래시 문자, 그리고 (구분자 없이) 문자 시퀀스 abcdef가 와요.

"\n\"\'\\abc\
def"

줄바꿈이 아닌 유니코드 줄 종결자(LF, VF, FF, CR, NEL, LS, PS)는 문자열 리터럴을 종료하는 것으로 간주돼요. 줄바꿈은 \가 앞에 오지 않는 경우에만 문자열 리터럴을 종료해요.

유니코드 리터럴 (Unicode Literals)

일반 문자열 리터럴은 ASCII만 포함할 수 있는 반면, unicode 키워드가 접두된 유니코드 리터럴은 유효한 UTF-8 시퀀스를 포함할 수 있어요. 일반 문자열 리터럴과 똑같은 이스케이프 시퀀스도 지원해요.

string memory a = unicode"Hello 😃";

16진수 리터럴 (Hexadecimal Literals)

16진수 리터럴은 hex 키워드가 접두되고 이중 또는 단일 따옴표로 감싸요(hex"001122FF", hex'0011_22_FF'). 그 내용은 16진수 자릿수여야 하며, 바이트 경계 사이의 구분자로 단일 밑줄을 선택적으로 사용할 수 있어요. 리터럴의 값은 16진수 시퀀스의 이진 표현일 거예요. 공백으로 구분된 여러 16진수 리터럴은 단일 리터럴로 연결돼요: hex"00112233" hex"44556677"은 hex"0011223344556677"과 동등해요. 16진수 리터럴은 어떤 면에서 문자열 리터럴처럼 동작하지만 string 타입으로 암시적으로 변환되지는 않아요.

열거형 (Enums)

Enum은 Solidity에서 사용자 정의 타입을 만드는 한 가지 방법이에요. 모든 정수 타입으로 명시적으로 변환될 수 있지만 암시적 변환은 허용되지 않아요. 정수에서의 명시적 변환은 값이 enum의 범위 안에 있는지 런타임에 검사하고, 그렇지 않으면 Panic 에러를 일으켜요. Enum은 적어도 하나의 멤버를 요구하며, 선언될 때 기본값은 첫 번째 멤버예요. Enum은 256개보다 많은 멤버를 가질 수 없어요.

데이터 표현은 C의 enum과 같아요: 옵션은 0부터 시작하는 연속된 부호 없는 정수 값으로 표현돼요. type(NameOfEnum).min과 type(NameOfEnum).max를 사용해 주어진 enum의 각각 가장 작은 값과 가장 큰 값을 얻을 수 있어요.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

contract test {
    enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
    ActionChoices choice;
    ActionChoices constant defaultChoice = ActionChoices.GoStraight;

    function setGoStraight() public {
        choice = ActionChoices.GoStraight;
    }

    // Since enum types are not part of the ABI, the signature of "getChoice"
    // will automatically be changed to "getChoice() returns (uint8)"
    // for all matters external to Solidity.
    function getChoice() public view returns (ActionChoices) {
        return choice;
    }

    function getDefaultChoice() public pure returns (uint) {
        return uint(defaultChoice);
    }

    function getLargestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).max;
    }

    function getSmallestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).min;
    }
}

참고: Enum은 컨트랙트나 라이브러리 정의 밖인 파일 수준에서도 선언할 수 있어요.

사용자 정의 값 타입 (User-defined Value Types)

사용자 정의 값 타입은 기본 값 타입에 대한 0 비용 추상화를 만들 수 있게 해요. 이는 별칭과 유사하지만 더 엄격한 타입 요구사항을 가져요. 사용자 정의 값 타입은 type C is V로 정의되는데, 여기서 C는 새로 도입된 타입의 이름이고 V는 내장 값 타입("밑바탕 타입")이어야 해요. C.wrap 함수는 밑바탕 타입에서 커스텀 타입으로 변환하는 데 사용돼요. 마찬가지로 C.unwrap 함수는 커스텀 타입에서 밑바탕 타입으로 변환하는 데 사용돼요.

타입 C는 어떤 연산자나 첨부된 멤버 함수도 가지지 않아요. 특히 == 연산자조차 정의되지 않아요. 다른 타입으로의/로부터의 명시적 및 암시적 변환은 허용되지 않아요. 이러한 타입 값의 데이터 표현은 밑바탕 타입에서 상속되고, 밑바탕 타입은 ABI에서도 사용돼요.

다음 예는 18자리의 십진 고정소수점 타입을 나타내는 커스텀 타입 UFixed256x18과 그 타입에 대한 산술 연산을 하는 최소 라이브러리를 보여줘요.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

// Represent a 18 decimal, 256 bit wide fixed point type using a user-defined value type.
type UFixed256x18 is uint256;

/// A minimal library to do fixed point operations on UFixed256x18.
library FixedMath {
    uint constant multiplier = 10**18;

    /// Adds two UFixed256x18 numbers. Reverts on overflow, relying on checked
    /// arithmetic on uint256.
    function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
    }
    /// Multiplies UFixed256x18 and uint256. Reverts on overflow, relying on checked
    /// arithmetic on uint256.
    function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
    }
    /// Take the floor of a UFixed256x18 number.
    /// @return the largest integer that does not exceed `a`.
    function floor(UFixed256x18 a) internal pure returns (uint256) {
        return UFixed256x18.unwrap(a) / multiplier;
    }
    /// Turns a uint256 into a UFixed256x18 of the same value.
    /// Reverts if the integer is too large.
    function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(a * multiplier);
    }
}

UFixed256x18.wrap과 FixedMath.toUFixed256x18이 같은 시그니처를 가지지만 매우 다른 두 연산을 수행한다는 점에 주목하세요: UFixed256x18.wrap 함수는 입력과 같은 데이터 표현을 가진 UFixed256x18을 반환하는 반면, toUFixed256x18은 같은 수치 값을 가진 UFixed256x18을 반환해요.

함수 타입 (Function Types)

함수 타입은 함수의 타입이에요. 함수 타입의 변수는 함수에서 할당될 수 있고, 함수 타입의 함수 파라미터는 함수 호출에 함수를 전달하고 함수 호출에서 함수를 반환하는 데 사용될 수 있어요.

함수 타입에는 내부(internal)와 외부(external) 함수의 두 가지 형태가 있어요:

  • 내부 함수는 현재 컨트랙트 안에서만 호출될 수 있어요(더 정확히는 현재 코드 단위 안에서, 내부 라이브러리 함수와 상속된 함수도 포함). 현재 컨트랙트의 컨텍스트 밖에서는 실행될 수 없기 때문이에요. 내부 함수 호출은 현재 컨트랙트의 함수를 내부적으로 호출하는 것처럼 그 진입 레이블로 점프함으로써 실현돼요.
  • 외부 함수는 주소와 함수 시그니처로 구성되며, 외부 함수 호출을 통해 전달되고 반환될 수 있어요.

현재 컨트랙트의 public 함수는 내부 함수와 외부 함수 둘 다로 사용될 수 있다는 점을 참고하세요. f를 내부 함수로 사용하려면 f를 사용하고, 외부 형태를 사용하려면 this.f를 사용해요. 함수 타입 변수가 초기화되지 않으면, 호출하면 Panic 에러가 발생해요. delete 후에 함수를 호출해도 같은 일이 발생해요.

참고: 람다 또는 인라인 함수는 계획되어 있지만 아직 지원되지 않아요.

선언 문법 (Declaration syntax)

함수 타입은 다음과 같이 표기돼요:

function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]

파라미터 타입과 대조적으로 반환 타입은 비어 있을 수 없어요. 함수 타입이 아무것도 반환하지 않아야 하면 전체 returns (<return types>) 부분을 생략해야 해요. 기본적으로 함수 타입은 내부형이므로 internal 키워드는 생략할 수 있어요. 이것은 함수 타입에만 적용된다는 점을 참고하세요. 컨트랙트에 정의된 함수에는 가시성을 명시적으로 지정해야 하며 기본값이 없어요.

변환 (Conversions)

함수 타입 A는 파라미터 타입이 동일하고, 반환 타입이 동일하고, 내부/외부 속성이 동일하고, A의 상태 가변성이 B의 상태 가변성보다 더 제한적인 경우에만 함수 타입 B로 암시적으로 변환될 수 있어요. 특히:

  • pure 함수는 view와 non-payable 함수로 변환될 수 있어요.
  • view 함수는 non-payable 함수로 변환될 수 있어요.
  • payable 함수는 non-payable 함수로 변환될 수 있어요.

함수 타입 사이의 다른 변환은 불가능해요. payable과 non-payable에 관한 규칙은 조금 혼란스러울 수 있지만, 본질적으로 함수가 payable이면 0 Ether의 지불도 받아들인다는 뜻이므로 non-payable이기도 해요. 반면 non-payable 함수는 보내진 Ether를 거부하므로, non-payable 함수는 payable 함수로 변환될 수 없어요. 명확히 하면, Ether를 거부하는 것은 거부하지 않는 것보다 더 제한적이에요. 즉 payable 함수를 non-payable로 오버라이드할 수 있지만 그 반대는 불가능해요.

추가로 non-payable 함수 포인터를 정의할 때 컴파일러는 가리키는 함수가 실제로 Ether를 거부할 것이라고 강제하지 않아요. 대신 함수 포인터가 Ether를 보내는 데 절대 사용되지 않도록 강제해요. 이는 payable 함수 포인터를 non-payable 함수 포인터에 할당해 두 타입이 같은 방식으로 동작하도록(즉 둘 다 Ether를 보내는 데 사용될 수 없도록) 보장하는 것을 가능하게 해요.

외부 함수 타입이 Solidity의 컨텍스트 밖에서 사용되면, 주소와 함수 식별자를 단일 bytes24 타입으로 함께 인코딩하는 function 타입으로 취급돼요. 내부 타입의 함수는 어디에 정의됐든 내부 함수 타입의 변수에 할당될 수 있어요. 여기에는 컨트랙트와 라이브러리의 private, internal, public 함수뿐 아니라 자유 함수(free functions)도 포함돼요. 반면 외부 함수 타입은 public 및 external 컨트랙트 함수와만 호환돼요.

참고: calldata 파라미터를 가진 외부 함수는 calldata 파라미터를 가진 외부 함수 타입과 호환되지 않아요. 대신 memory 파라미터를 가진 해당 타입과 호환돼요. 예를 들어 function (string calldata) external 타입의 값이 가리킬 수 있는 함수는 없지만, function (string memory) external은 function f(string memory) external {}와 function g(string calldata) external {} 둘 다를 가리킬 수 있어요. 두 위치 모두에서 인자가 같은 방식으로 함수에 전달되기 때문이에요. 호출자는 자신의 calldata를 외부 함수에 직접 전달할 수 없고 항상 인자를 memory로 ABI-인코딩해요. 파라미터를 calldata로 표시하는 것은 외부 함수의 구현에만 영향을 주고 호출자 쪽 함수 포인터에서는 의미가 없어요.

경고: 내부 함수 포인터 비교는 최적화 프로그램이 활성화된 레거시 파이프라인에서 예상치 못한 결과를 낳을 수 있어요. 최적화 프로그램이 동일한 함수를 하나로 합칠 수 있고, 그러면 상기 함수 포인터가 같지 않은 대신 같다고 비교되기 때문이에요. 그러한 비교는 권장되지 않으며, 경고가 오류로 승격되는 다음 breaking 릴리스(0.9.0)까지 컴파일러가 경고를 발행할 거예요. 라이브러리는 delegatecall이 필요하고 선택자에 다른 ABI 규칙을 사용하므로 제외돼요. 인터페이스에 선언된 함수는 정의가 없으므로 그것을 가리키는 것도 의미가 없어요.

멤버 (Members)

외부(또는 public) 함수는 다음 멤버를 가져요:

  • .address 는 함수 컨트랙트의 주소를 반환해요.
  • .selector 는 ABI 함수 선택자를 반환해요.

참고: 외부(또는 public) 함수는 추가 멤버 .gas(uint)와 .value(uint)를 가지고 있었어요. 이들은 Solidity 0.6.2에서 더 이상 사용되지 않고 0.7.0에서 제거됐어요. 대신 함수에 보내질 가스량이나 wei 양을 지정하려면 {gas: ...}와 {value: ...}를 사용하세요. 자세한 내용은 외부 함수 호출을 보세요.

컨트랙트 업데이트에 걸친 값 안정성 (Value stability across contract updates)

함수 타입 값을 사용할 때 고려해야 할 중요한 측면은 밑바탕 코드가 바뀌어도 값이 유효하게 유지될지 여부예요. 블록체인의 상태는 완전히 불변하지 않고 같은 주소 아래 다른 코드를 두는 여러 방법이 있어요:

  • 랜덤 시드 컨트랙트 생성(salted contract creation)을 사용해 다른 코드를 직접 배포.
  • DELEGATECALL을 통해 다른 컨트랙트에 위임(프록시 컨트랙트 뒤의 업그레이드 가능한 코드가 일반적인 예).
  • EIP-7702가 정의한 계정 추상화.

외부 함수 타입은 컨트랙트의 ABI만큼 안정적인 것으로 간주될 수 있어서 매우 이식 가능해요. 그 ABI 표현은 항상 컨트랙트 주소와 함수 선택자로 구성되며, 장기간 저장하거나 컨트랙트 간에 전달하는 것이 완전히 안전해요. 참조된 함수가 변경되거나 사라질 수는 있지만 직접 외부 호출도 같은 방식으로 영향을 받으므로, 그러한 사용에 추가 위험은 없어요.

그러나 내부 함수의 경우 값은 컨트랙트 바이트코드에 강하게 묶인 식별자예요. 식별자의 실제 표현은 구현 세부 사항이며 컴파일러 버전이나 다른 백엔드 사이에서 바뀔 수 있어요. 주어진 표현 아래 할당된 값은 결정적이지만(소스 코드가 같으면 동일하게 유지된다고 보장), 함수의 추가, 제거, 재정렬 같은 변경에 쉽게 영향을 받아요. 컴파일러는 또한 절대 사용되지 않는 내부 함수를 제거할 자유가 있는데, 이는 다른 식별자에 영향을 줄 수 있어요. 식별자가 단순히 점프 대상인 표현 같은 일부 표현은 사실상 어떤 변경에도 영향을 받을 수 있는데, 심지어 내부 함수와 완전히 무관한 변경에도요.

이에 대응하기 위해 언어는 내부 함수 타입의 유효한 컨텍스트 밖에서의 사용을 제한해요. 그래서 내부 함수 타입은 외부 함수의 파라미터(또는 컨트랙트의 ABI에 노출되는 어떤 다른 방식)로 사용될 수 없어요. 그러나 여전히 사용이 안전한지 여부를 사용자가 결정해야 하는 상황이 있어요. 예를 들어 그러한 값을 상태 변수에 장기간 저장하는 것은 권장되지 않지만, 컨트랙트 코드가 결코 업데이트되지 않을 것이라면 안전할 수 있어요. 인라인 어셈블리를 사용해 어떤 안전장치든 항상 우회하는 것도 가능해요. 그러한 사용은 항상 신중한 고려가 필요해요.

참고: 사용되지 않는 내부 함수의 제거는 그러한 함수에 대한 이름으로 된 명시적 참조만 고려해요. 인라인 어셈블리에서 함수 타입 변수에 새 값을 할당하는 것 같은 암시적 참조는, 해당 함수가 소스의 다른 곳에서도 명시적으로 참조되지 않으면 함수의 제거로 이어질 수 있어요.

예시 (Examples)

멤버를 사용하는 방법을 보여주는 예:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.4 <0.9.0;

contract Example {
    function f() public payable returns (bytes4) {
        assert(this.f.address == address(this));
        return this.f.selector;
    }

    function g() public {
        this.f{gas: 10, value: 800}();
    }
}

내부 함수 타입을 사용하는 방법을 보여주는 예:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

library ArrayUtils {
    // internal functions can be used in internal library functions because
    // they will be part of the same code context
    function map(uint[] memory self, function (uint) pure returns (uint) f)
        internal
        pure
        returns (uint[] memory r)
    {
        r = new uint[](self.length);
        for (uint i = 0; i < self.length; i++) {
            r[i] = f(self[i]);
        }
    }

    function reduce(
        uint[] memory self,
        function (uint, uint) pure returns (uint) f
    )
        internal
        pure
        returns (uint r)
    {
        r = self[0];
        for (uint i = 1; i < self.length; i++) {
            r = f(r, self[i]);
        }
    }

    function range(uint length) internal pure returns (uint[] memory r) {
        r = new uint[](length);
        for (uint i = 0; i < r.length; i++) {
            r[i] = i;
        }
    }
}

contract Pyramid {
    using ArrayUtils for *;

    function pyramid(uint l) public pure returns (uint) {
        return ArrayUtils.range(l).map(square).reduce(sum);
    }

    function square(uint x) internal pure returns (uint) {
        return x * x;
    }

    function sum(uint x, uint y) internal pure returns (uint) {
        return x + y;
    }
}

외부 함수 타입을 사용하는 또 다른 예:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract Oracle {
    struct Request {
        bytes data;
        function(uint) external callback;
    }

    Request[] private requests;
    event NewRequest(uint);

    function query(bytes memory data, function(uint) external callback) public {
        requests.push(Request(data, callback));
        emit NewRequest(requests.length - 1);
    }

    function reply(uint requestID, uint response) public {
        // Here goes the check that the reply comes from a trusted source
        requests[requestID].callback(response);
    }
}

contract OracleUser {
    Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract
    uint private exchangeRate;

    function buySomething() public {
        ORACLE_CONST.query("USD", this.oracleResponse);
    }

    function oracleResponse(uint response) public {
        require(
            msg.sender == address(ORACLE_CONST),
            "Only oracle can call this."
        );
        exchangeRate = response;
    }
}

참조 타입 (Reference Types)

참조 타입의 값은 여러 다른 이름을 통해 수정될 수 있어요. 값 타입의 변수가 사용될 때마다 독립적인 복사본을 얻는 것과 대조적이에요. 그 때문에 참조 타입은 값 타입보다 더 신중하게 다뤄야 해요. 현재 참조 타입은 구조체(struct), 배열(array), 매핑(mapping)으로 구성돼요. 참조 타입을 사용할 때는 항상 타입이 저장되는 데이터 영역을 명시적으로 제공해야 해요: memory(수명이 외부 함수 호출로 제한됨), storage(상태 변수가 저장되는 위치, 수명이 컨트랙트의 수명으로 제한됨), calldata(함수 인자를 포함하는 특수 데이터 위치). 데이터 위치를 바꾸는 할당이나 타입 변환은 항상 자동 복사 연산을 수반하는 반면, 같은 데이터 위치 안의 할당은 스토리지 타입의 경우에만 일부 경우에 복사해요.

데이터 위치 (Data location)

모든 참조 타입에는 어디에 저장되는지에 대한 추가 주석인 "데이터 위치"가 있어요. 세 가지 데이터 위치가 있어요: memory, storage, calldata. calldata는 함수 인자가 저장되는 수정 불가능하고 비영구적인 영역이며, 대부분 memory처럼 동작해요.

참고: transient는 아직 참조 타입의 데이터 위치로 지원되지 않아요. 참고: 가능하면 calldata를 데이터 위치로 사용해 복사를 피하고 데이터가 수정될 수 없도록 하세요. calldata 데이터 위치를 가진 배열과 구조체는 함수에서 반환될 수도 있지만, 그러한 타입을 할당하는 것은 불가능해요. 참고: 함수 본문에 선언되거나 반환 파라미터로 선언된 calldata 위치의 배열과 구조체는 사용되거나 반환되기 전에 할당되어야 해요. 사소하지 않은 제어 흐름이 사용되고 컴파일러가 초기화를 제대로 감지하지 못하는 특정 경우가 있어요. 그러한 경우의 일반적인 해결책은 올바른 초기화가 일어나기 전에 해당 변수를 자기 자신에게 할당하는 것이에요. 참고: 0.6.9 버전 이전에는 참조 타입 인자의 데이터 위치가 외부 함수에서는 calldata, public 함수에서는 memory, 내부 및 private 함수에서는 memory 또는 storage로 제한됐어요. 이제는 모든 함수에서 가시성과 무관하게 memory와 calldata가 허용돼요. 참고: 생성자(constructor) 파라미터는 데이터 위치로 calldata를 사용할 수 없어요. 참고: 0.5.0 버전 이전에는 데이터 위치를 생략할 수 있었고 변수의 종류, 함수 타입 등에 따라 다른 위치가 기본값이었지만, 이제 모든 복합 타입은 명시적 데이터 위치를 제공해야 해요.

데이터 위치와 할당 동작 (Data location and assignment behavior)

데이터 위치는 데이터의 영속성뿐 아니라 할당의 의미에도 관련돼요:

  • storage와 memory 사이의 할당(calldata에서도)은 항상 독립적인 복사본을 만들어요.
  • memory에서 memory로의 할당은 참조만 만들어요. 즉 한 메모리 변수에 대한 변경이 같은 데이터를 참조하는 다른 모든 메모리 변수에도 보여요.
  • storage에서 로컬 storage 변수로의 할당도 참조만 할당해요.
  • storage에 대한 다른 모든 할당은 항상 복사해요. 이 경우의 예로는 상태 변수에 대한 할당이나 로컬 변수 자체가 단지 참조일지라도 storage 구조체 타입의 로컬 변수 멤버에 대한 할당이 있어요.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
    // The data location of x is storage.
    // This is the only place where the
    // data location can be omitted.
    uint[] x;

    // The data location of memoryArray is memory.
    function f(uint[] memory memoryArray) public {
        x = memoryArray; // works, copies the whole array to storage
        uint[] storage y = x; // works, assigns a pointer, data location of y is storage
        y[7]; // fine, returns the 8th element
        y.pop(); // fine, modifies x through y
        delete x; // fine, clears the array, also modifies y
        // The following does not work; it would need to create a new temporary /
        // unnamed array in storage, but storage is "statically" allocated:
        // y = memoryArray;
        // Similarly, "delete y" is not valid, as assignments to local variables
        // referencing storage objects can only be made from existing storage objects.
        // It would "reset" the pointer, but there is no sensible location it could point to.
        // For more details see the documentation of the "delete" operator.
        // delete y;
        g(x); // calls g, handing over a reference to x
        h(x); // calls h and creates an independent, temporary copy in memory
    }

    function g(uint[] storage) internal pure {}
    function h(uint[] memory) public pure {}
}

배열 (Arrays)

배열은 컴파일 타임 고정 크기를 가지거나 동적 크기를 가질 수 있어요. 고정 크기 k와 요소 타입 T의 배열 타입은 T[k]로, 동적 크기의 배열은 T[]로 작성돼요. 예를 들어 5개의 uint 동적 배열의 배열은 uint[][5]로 작성돼요. 표기법은 일부 다른 언어에 비해 반대예요. Solidity에서 X[3]은 X가 배열 자체여도 항상 타입 X의 요소 3개를 가진 배열이에요. C 같은 다른 언어에서는 그렇지 않아요.

인덱스는 0부터 시작하고, 접근은 선언과 반대 방향이에요. 예를 들어 변수 uint[][5] memory x가 있으면 x[2][6]으로 세 번째 동적 배열의 일곱 번째 uint에 접근하고, 세 번째 동적 배열에 접근하려면 x[2]를 사용해요. 다시 말해 배열일 수도 있는 타입 T에 대해 배열 T[5] a가 있으면 a[2]는 항상 타입 T예요.

배열 요소는 매핑이나 구조체를 포함한 어떤 타입이든 될 수 있어요. 일반 타입 제한이 적용되며, 매핑은 스토리지 데이터 위치에만 저장될 수 있고 공개적으로 보이는 함수는 ABI 타입인 파라미터가 필요해요. 상태 변수 배열을 public으로 표시하고 Solidity가 getter를 만들게 할 수 있어요. 숫자 인덱스는 getter의 필수 파라미터가 돼요. 배열의 끝을 지나 접근하면 실패한 어설션이 발생해요. .push()와 .push(value) 메서드를 사용해 동적 크기 배열의 끝에 새 요소를 추가할 수 있어요. .push()는 0으로 초기화된 요소를 추가하고 그에 대한 참조를 반환해요.

참고: 동적 크기 배열은 스토리지에서만 크기를 조정할 수 있어요. memory에서는 그러한 배열이 임의 크기를 가질 수 있지만, 배열이 할당되면 크기를 바꿀 수 없어요.

바이트와 문자열을 배열로 (bytes and string as Arrays)

bytes와 string 타입의 변수는 특수한 배열이에요. bytes 타입은 bytes1[]과 유사하지만, calldata와 memory에서 빽빽하게 패킹돼요. string은 bytes와 같지만 길이나 인덱스 접근을 허용하지 않아요.

Solidity에는 문자열 조작 함수가 없지만, 타사 문자열 라이브러리가 있어요. 또한 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))를 사용해 두 문자열을 keccak256-해시로 비교하고, string.concat(s1, s2)를 사용해 두 문자열을 연결할 수 있어요.

bytes1[]보다 bytes를 사용해야 해요. bytes1[]을 memory에서 사용하면 요소 사이에 31개의 패딩 바이트가 추가되기 때문에 더 저렴하거든요. 스토리지에서는 빽빽한 패킹 때문에 패딩이 없다는 점을 참고하세요(바이트와 문자열 참고). 일반 규칙으로, 임의 길이의 원시 바이트 데이터에는 bytes를, 임의 길이의 문자열(UTF-8) 데이터에는 string을 사용해요. 길이를 특정 바이트 수로 제한할 수 있다면 값 타입 bytes1부터 bytes32 중 하나를 항상 사용하세요. 훨씬 더 저렴하니까요.

참고: 문자열 s의 바이트 표현에 접근하려면 bytes(s).length / bytes(s)[7] = 'x';를 사용해요. UTF-8 표현의 저수준 바이트에 접근하는 것이지 개별 문자에 접근하는 것이 아님을 명심하세요.

bytes.concat과 string.concat 함수 (The functions bytes.concat and string.concat)

string.concat을 사용해 임의 개수의 string 값을 연결할 수 있어요. 이 함수는 인자의 내용을 패딩 없이 담은 단일 string memory 배열을 반환해요. string으로 암시적으로 변환되지 않는 다른 타입의 파라미터를 사용하려면, 먼저 string으로 변환해야 해요. 마찬가지로 bytes.concat 함수는 임의 개수의 bytes 또는 bytes1 ... bytes32 값을 연결할 수 있어요. 이 함수는 인자의 내용을 패딩 없이 담은 단일 bytes memory 배열을 반환해요. bytes로 암시적으로 변환되지 않는 string 파라미터나 다른 타입을 사용하려면, 먼저 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 배열은 new 연산자를 사용해 만들 수 있어요. storage 배열과 달리 memory 배열의 크기를 조정하는 것은 불가능해요(예: .push 멤버 함수를 사용할 수 없어요). 필요한 크기를 미리 계산하거나, 새 memory 배열을 만들어 각 요소를 복사해야 해요. 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)

배열 리터럴은 대괄호([...])로 감싼 하나 이상의 표현식의 쉼표로 구분된 목록이에요. 예를 들어 [1, a, f(3)]. 배열 리터럴의 타입은 다음과 같이 결정돼요: 항상 길이가 표현식의 수인 정적 크기 memory 배열이에요. 배열의 밑 타입은 다른 모든 표현식이 암시적으로 그 타입으로 변환될 수 있도록 목록의 첫 번째 표현식의 타입이에요. 이것이 불가능하면 타입 오류예요. 모든 요소가 변환될 수 있는 타입이 있는 것만으로는 충분하지 않아요. 요소 중 하나가 그 타입이어야 해요.

아래 예에서 [1, 2, 3]의 타입은 uint8[3] memory예요. 이 상수 각각의 타입이 uint8이기 때문이에요. 결과가 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이고 두 번째의 타입이 int8이며 서로 암시적으로 변환될 수 없기 때문에 유효하지 않아요. 예를 들어 [int8(1), -1]을 사용해 작동하게 만들 수 있어요.

다른 타입의 고정 크기 memory 배열은 (밑 타입이 변환 가능하더라도) 서로 변환될 수 없으므로, 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;
    }
}

고정 크기 memory 배열은 동적 크기 memory 배열에 할당될 수 없어요. 즉 다음은 불가능해요:

// 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: 배열에는 요소의 수를 담는 length 멤버가 있어요. memory 배열의 길이는 생성되면 고정(하지만 동적, 즉 런타임 파라미터에 의존할 수 있음)돼요.

push(): 동적 storage 배열과 bytes(string 아님)에는 push()라는 멤버 함수가 있어서 배열 끝에 0으로 초기화된 요소를 추가할 수 있어요. 요소에 대한 참조를 반환하므로 x.push().t = 2 또는 x.push() = b처럼 사용할 수 있어요.

push(x): 동적 storage 배열과 bytes(string 아님)에는 push(x)라는 멤버 함수가 있어서 배열 끝에 주어진 요소를 추가할 수 있어요. 함수는 아무것도 반환하지 않아요.

pop(): 동적 storage 배열과 bytes(string 아님)에는 pop()이라는 멤버 함수가 있어서 배열 끝에서 요소를 제거할 수 있어요. 이것은 또한 제거된 요소에 delete를 암시적으로 호출해요. 함수는 아무것도 반환하지 않아요.

참고: push()를 호출해 storage 배열의 길이를 늘리는 것은 storage가 0으로 초기화되기 때문에 상수 가스 비용이 들지만, pop()을 호출해 길이를 줄이는 것은 제거되는 요소의 "크기"에 의존하는 비용이 들어요. 그 요소가 배열이면, 제거된 요소를 명시적으로 지우는 것(delete)과 유사한 비용이 포함되므로 매우 비쌀 수 있어요. 참고: 외부(public 대신) 함수에서 배열의 배열을 사용하려면 ABI coder v2를 활성화해야 해요. 참고: Byzantium 이전의 EVM 버전에서는 함수 호출에서 반환된 동적 배열에 접근할 수 없었어요. 동적 배열을 반환하는 함수를 호출한다면 EVM이 Byzantium 모드로 설정되어 있는지 확인하세요.

// 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
    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;
    }
}
Storage 배열 요소에 대한 댕글링 참조 (Dangling References to Storage Array Elements)

storage 배열로 작업할 때 댕글링 참조를 피하도록 주의해야 해요. 댕글링 참조는 더 이상 존재하지 않거나 참조를 갱신하지 않고 이동된 무언가를 가리키는 참조예요. 댕글링 참조는 예를 들어 배열 요소에 대한 참조를 로컬 변수에 저장한 다음 포함하는 배열에서 .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의 유효한 요소를 가리키지 않음에도 되돌아가지 않아요. 컴파일러는 사용되지 않는 storage가 항상 0이 된다고 가정하므로, 이후의 s.push()는 storage에 0을 명시적으로 쓰지 않아서, 그 push() 후 s의 마지막 요소는 길이 1이고 첫 요소로 0x42를 가질 거예요.

Solidity가 storage에서 값 타입에 대한 참조 선언을 허용하지 않는다는 점을 참고하세요. 이러한 종류의 명시적 댕글링 참조는 중첩 참조 타입으로 제한돼요. 그러나 튜플 할당에서 복잡한 표현식을 사용할 때 댕글링 참조가 일시적으로 발생할 수도 있어요:

// 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;
    }
}

문장당 한 번만 storage에 할당하고 할당의 왼쪽에 복잡한 표현식을 피하는 것이 항상 더 안전해요. bytes 배열의 요소에 대한 참조를 다룰 때 특별한 주의가 필요한데, bytes 배열의 .push()가 스토리지에서 짧은 배치에서 긴 배치로 전환할 수 있기 때문이에요.

// 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의 첫 storage 슬롯의 요소에 대한 참조를 반환해요. 그러나 두 번째 x.push()는 bytes 배열을 큰(large) 배치로 전환해요. 이제 x.push()가 참조한 요소는 배열의 데이터 영역에 있는 반면, 참조는 여전히 원래 위치를 가리키는데 그 위치는 이제 길이 필드의 일부가 되고, 할당은 효과적으로 x의 길이를 엉망으로 만들 거예요. 안전하려면 단일 할당 동안 bytes 배열을 최대 한 요소만 확장하고, 같은 문장에서 배열에 동시에 인덱스 접근하지 마세요.

위는 현재 컴파일러 버전에서 storage 댕글링 참조의 동작을 설명하지만, 댕글링 참조가 있는 어떤 코드든 정의되지 않은 동작을 가진 것으로 간주해야 해요. 특히 이는 컴파일러의 어떤 미래 버전이든 댕글링 참조를 포함하는 코드의 동작을 바꿀 수 있음을 의미해요. 코드에서 댕글링 참조를 확실히 피하세요!

배열 슬라이스 (Array Slices)

배열 슬라이스는 배열의 연속적인 부분에 대한 뷰예요. x[start:end]로 작성되는데, start와 end는 uint256 타입으로 귀결되는(또는 암시적으로 변환되는) 표현식이에요. 슬라이스의 첫 요소는 x[start]이고 마지막 요소는 x[end - 1]이에요. start가 end보다 크거나 end가 배열의 길이보다 크면 예외가 던져져요. start와 end 모두 선택 사항이에요: start는 기본적으로 0, end는 기본적으로 배열의 길이예요.

배열 슬라이스는 멤버가 없어요. 밑바탕 타입의 배열로 암시적으로 변환 가능하고 인덱스 접근을 지원해요. 인덱스 접근은 밑바탕 배열에서 절대적이지 않고 슬라이스의 시작에 상대적이에요. 배열 슬라이스는 타입 이름이 없어서 어떤 변수도 타입으로 배열 슬라이스를 가질 수 없으며, 중간 표현식에만 존재해요.

참고: 현재 배열 슬라이스는 calldata 배열에만 구현돼 있어요.

배열 슬라이스는 함수 파라미터로 전달된 보조 데이터를 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 behavior, 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는 구조체의 형태로 새 타입을 정의하는 방법을 제공해요. 다음 예에서 보여줘요:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <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;
        (bool success, ) = c.beneficiary.call{value: amount}("");
        return success;
    }
}

이 컨트랙트는 크라우드펀딩 컨트랙트의 완전한 기능을 제공하지는 않지만, 구조체를 이해하는 데 필요한 기본 개념을 포함해요. 구조체 타입은 매핑과 배열 안에서 사용될 수 있고, 스스로 매핑과 배열을 포함할 수 있어요. 구조체가 자신의 타입의 멤버를 포함하는 것은 불가능하지만, 구조체 자체가 매핑 멤버의 값 타입이 되거나 자신 타입의 동적 크기 배열을 포함할 수는 있어요. 구조체의 크기가 유한해야 하므로 이 제한이 필요해요.

모든 함수에서 구조체 타입이 데이터 위치 storage인 로컬 변수에 할당되는 방법을 주목하세요. 이것은 구조체를 복사하지 않고 참조만 저장해서, 로컬 변수의 멤버에 대한 할당이 실제로 상태에 쓰이도록 해요. 물론 campaigns[campaignID].amount = 0처럼 로컬 변수에 할당하지 않고 구조체의 멤버에 직접 접근할 수도 있어요.

참고: Solidity 0.7.0까지는 storage-only 타입(예: 매핑)의 멤버를 포함한 memory-구조체가 허용됐고, 위 예의 campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0) 같은 할당이 작동하며 그 멤버들을 조용히 건너뛰었어요.

매핑 타입 (Mapping Types)

매핑 타입은 mapping(KeyType KeyName? => ValueType ValueName?) 문법을 사용하고, 매핑 타입의 변수는 mapping(KeyType KeyName? => ValueType ValueName?) VariableName 문법으로 선언돼요. KeyType는 어떤 내장 값 타입, bytes, string, 또는 어떤 컨트랙트나 enum 타입이 될 수 있어요. 매핑, 구조체, 배열 타입 같은 다른 사용자 정의나 복합 타입은 허용되지 않아요. ValueType는 매핑, 배열, 구조체를 포함한 어떤 타입이든 될 수 있어요. KeyName과 ValueName은 선택 사항이고(mapping(KeyType => ValueType)도 작동), 타입이 아닌 유효한 식별자라면 무엇이든 될 수 있어요.

매핑을 해시 테이블로 생각할 수 있어요. 사실상 모든 가능한 키가 존재하고 바이트 표현이 모두 0인 값(타입의 기본값)에 매핑되도록 초기화돼요. 유사성은 거기서 끝나요. 키 데이터는 매핑에 저장되지 않고, 값 조회에 keccak256 해시만 사용돼요. 이 때문에 매핑은 길이도, 키나 값이 설정된다는 개념도 없어서, 할당된 키에 대한 추가 정보 없이는 지워질 수 없어요(매핑 지우기 참고).

매핑은 storage 데이터 위치만 가질 수 있고, 따라서 상태 변수, 함수의 storage 참조 타입, 라이브러리 함수의 파라미터로 허용돼요. 공개적으로 보이는 컨트랙트 함수의 파라미터나 반환 파라미터로는 사용될 수 없어요. 이 제한은 매핑을 포함하는 배열과 구조체에도 마찬가지로 적용돼요.

매핑 타입의 상태 변수를 public으로 표시하면 Solidity가 getter를 만들어줘요. KeyType가 getter의 KeyName(지정된 경우)이라는 이름의 파라미터가 돼요. ValueType이 값 타입이나 구조체이면 getter는 ValueName(지정된 경우)이라는 이름의 ValueType을 반환해요. ValueType이 배열이나 매핑이면 getter는 각 KeyType에 대해 파라미터를 하나씩 (재귀적으로) 가져요.

아래 예에서 MappingExample 컨트랙트는 키 타입이 address, 값 타입이 uint인 public balances 매핑을 정의해, 이더리움 주소를 부호 없는 정수 값에 매핑해요. uint는 값 타입이므로 getter는 타입과 일치하는 값을 반환하는데, 이것을 MappingUser 컨트랙트에서 지정된 주소의 값을 반환하는 것으로 볼 수 있어요.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

contract MappingUser {
    function f() public returns (uint) {
        MappingExample m = new MappingExample();
        m.update(100);
        return m.balances(address(this));
    }
}

아래 예는 ERC20 토큰의 단순화된 버전이에요. _allowances는 다른 매핑 타입 안에 있는 매핑 타입의 예시예요. 아래 예에서는 매핑에 선택적 KeyName과 ValueName이 제공돼요. 이는 컨트랙트 기능이나 바이트코드에 영향을 주지 않고, 매핑 getter에 대한 ABI에서 inputs와 outputs의 name 필드만 설정해요.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.18;

contract MappingExampleWithNames {
    mapping(address user => uint balance) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

아래 예는 _allowances를 사용해 다른 사람이 당신 계정에서 출금할 수 있는 금액을 기록해요.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract MappingExample {

    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }

    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
        require(_allowances[sender][msg.sender] >= amount, "ERC20: Allowance not high enough.");
        _allowances[sender][msg.sender] -= amount;
        _transfer(sender, recipient, amount);
        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
        require(_balances[sender] >= amount, "ERC20: Not enough funds.");

        _balances[sender] -= amount;
        _balances[recipient] += amount;
        emit Transfer(sender, recipient, amount);
    }
}

순회 가능한 매핑 (Iterable Mappings)

매핑을 순회할 수 없어요. 즉 그 키를 열거할 수 없어요. 그러나 매핑 위에 데이터 구조를 구현하고 그것을 순회하는 것은 가능해요. 예를 들어 아래 코드는 User 컨트랙트가 데이터를 추가하는 IterableMapping 라이브러리를 구현하고, sum 함수가 모든 값을 합산하기 위해 순회해요.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }

struct itmap {
    mapping(uint => IndexValue) data;
    KeyFlag[] keys;
    uint size;
}

type Iterator is uint;

library IterableMapping {
    function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
        uint keyIndex = self.data[key].keyIndex;
        self.data[key].value = value;
        if (keyIndex > 0)
            return true;
        else {
            keyIndex = self.keys.length;
            self.keys.push();
            self.data[key].keyIndex = keyIndex + 1;
            self.keys[keyIndex].key = key;
            self.size++;
            return false;
        }
    }

    function remove(itmap storage self, uint key) internal returns (bool success) {
        uint keyIndex = self.data[key].keyIndex;
        if (keyIndex == 0)
            return false;
        delete self.data[key];
        self.keys[keyIndex - 1].deleted = true;
        self.size --;
    }

    function contains(itmap storage self, uint key) internal view returns (bool) {
        return self.data[key].keyIndex > 0;
    }

    function iterateStart(itmap storage self) internal view returns (Iterator) {
        return iteratorSkipDeleted(self, 0);
    }

    function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) {
        return Iterator.unwrap(iterator) < self.keys.length;
    }

    function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) {
        return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1);
    }

    function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) {
        uint keyIndex = Iterator.unwrap(iterator);
        key = self.keys[keyIndex].key;
        value = self.data[key].value;
    }

    function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) {
        while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
            keyIndex++;
        return Iterator.wrap(keyIndex);
    }
}

// How to use it
contract User {
    // Just a struct holding our data.
    itmap data;
    // Apply library functions to the data type.
    using IterableMapping for itmap;

    // Insert something
    function insert(uint k, uint v) public returns (uint size) {
        // This calls IterableMapping.insert(data, k, v)
        data.insert(k, v);
        // We can still access members of the struct,
        // but we should take care not to mess with them.
        return data.size;
    }

    // Computes the sum of all stored data.
    function sum() public view returns (uint s) {
        for (
            Iterator i = data.iterateStart();
            data.iterateValid(i);
            i = data.iterateNext(i)
        ) {
            (, uint value) = data.iterateGet(i);
            s += value;
        }
    }
}

연산자 (Operators)

산술 및 비트 연산자는 두 피연산자의 타입이 같지 않아도 적용될 수 있어요. 예를 들어 x가 uint8이고 z가 uint32 타입일 때 y = x + z를 계산할 수 있어요. 이러한 경우 연산이 계산되는 타입(오버플로우의 경우 중요)과 연산자 결과의 타입을 결정하는 다음 메커니즘이 사용돼요:

  • 오른쪽 피연산자의 타입이 왼쪽 피연산자의 타입으로 암시적으로 변환될 수 있으면 왼쪽 피연산자의 타입을 사용해요.
  • 왼쪽 피연산자의 타입이 오른쪽 피연산자의 타입으로 암시적으로 변환될 수 있으면 오른쪽 피연산자의 타입을 사용해요.
  • 그렇지 않으면 연산이 허용되지 않아요.

피연산자 중 하나가 리터럴 숫자이면 먼저 그 "mobile type"으로 변환되는데, 이는 값을 담을 수 있는 가장 작은 타입이에요(같은 비트 폭의 부호 없는 타입이 부호 있는 타입보다 "작은" 것으로 간주돼요). 둘 다 리터럴 숫자이면, 결과가 비리터럴 타입과 함께 사용될 때 어떤 것도 잃지 않도록 표현식이 필요한 정밀도로 평가되는, 사실상 무제한 정밀도로 연산이 계산돼요.

연산자의 결과 타입은 연산이 수행되는 타입과 같아요. 단, 결과가 항상 bool인 비교 연산자는 예외예요. 연산자 **(지수), <<, >>는 연산과 결과에 왼쪽 피연산자의 타입을 사용해요.

삼항 연산자 (Ternary Operator)

삼항 연산자는 <expression> ? <trueExpression> : <falseExpression> 형태의 표현식에 사용돼요. 메인 <expression>의 평가 결과에 따라 주어진 두 표현식 중 하나를 평가해요. <expression>이 true로 평가되면 <trueExpression>이, 그렇지 않으면 <falseExpression>이 평가돼요.

삼항 연산자의 결과는 모든 피연산자가 유리수 리터럴이어도 유리수 타입이 아니에요. 결과 타입은 위와 같은 방식으로 두 피연산자의 타입에서 결정되며, 필요하면 먼저 mobile type으로 변환해요. 결과적으로 255 + (true ? 1 : 0)은 산술 오버플로우 때문에 되돌아가요. 이유는 (true ? 1 : 0)이 uint8 타입이라 덧셈도 uint8로 강제되고, 256은 이 타입에 허용된 범위를 초과하기 때문이에요. 또 다른 결과로 1.5 + 1.5 같은 표현식은 유효하지만 1.5 + (true ? 1.5 : 2.5)는 유효하지 않아요. 전자는 무제한 정밀도로 평가된 유리수 표현식이고 최종 값만 중요하기 때문이에요. 후자는 분수 유리수를 정수로 변환하는 것을 수반하는데, 현재는 허용되지 않아요.

복합 및 증가/감소 연산자 (Compound and Increment/Decrement Operators)

a가 LValue(즉 변수나 할당될 수 있는 것)이면 다음 연산자를 줄임말로 사용할 수 있어요: a += e는 a = a + e와 동등해요. 연산자 -=, *=, /=, %=, |=, &=, ^=, <<=, >>=도 이에 따라 정의돼요. a++와 a--는 a += 1 / a -= 1과 동등하지만 표현식 자체는 여전히 a의 이전 값을 가져요. 대조적으로 --a와 ++a는 a에 같은 효과를 주지만 변경 후의 값을 반환해요.

delete

delete a는 타입의 초기 값을 a에 할당해요. 즉 정수의 경우 a = 0과 동등하지만, 배열에도 사용할 수 있는데 길이 0의 동적 배열이나 모든 요소가 초기 값으로 설정된 같은 길이의 정적 배열을 할당해요. delete a[x]는 배열의 인덱스 x의 항목을 삭제하고 다른 모든 요소와 배열의 길이는 그대로 둬요. 특히 배열에 간격을 남긴다는 뜻이에요. 항목을 제거할 계획이라면 매핑이 더 나은 선택일 수 있어요. 구조체의 경우 모든 멤버가 리셋된 구조체를 할당해요. 다시 말해 delete a 후의 a 값은 할당 없이 a가 선언된 것과 같아요. 다음 주의사항이 있어요: delete는 매핑에 효과가 없어요(매핑의 키는 임의일 수 있고 일반적으로 알려져 있지 않기 때문). 그래서 구조체를 delete하면 매핑이 아닌 모든 멤버를 리셋하고, 매핑이 아닌 한 멤버로 재귀하기도 해요. 그러나 개별 키와 그 키가 매핑하는 것은 삭제할 수 있어요: a가 매핑이면 delete a[x]는 x에 저장된 값을 삭제해요.

delete a가 정말로 a에 대한 할당처럼 동작한다는 것, 즉 a에 새 객체를 저장한다는 것을 주목하는 것이 중요해요. 이 구분은 a가 참조 변수일 때 보여요: a 자체만 리셋할 뿐 이전에 참조했던 값은 리셋하지 않아요.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract DeleteExample {
    uint data;
    uint[] dataArray;

    function f() public {
        uint x = data;
        delete x; // sets x to 0, does not affect data
        delete data; // sets data to 0, does not affect x
        uint[] storage y = dataArray;
        delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also
        // y is affected which is an alias to the storage object
        // On the other hand: "delete y" is not valid, as assignments to local variables
        // referencing storage objects can only be made from existing storage objects.
        assert(y.length == 0);
    }
}

연산자 우선순위 (Order of Precedence of Operators)

다음은 연산자의 우선순위 순서로, 평가 순서로 나열돼요.

우선순위 설명 연산자
1 후위 증가와 감소 ++, --
new 표현식 new <typename>
배열 첨자 <array>[<index>]
멤버 접근 <object>.<member>
함수형 호출 <func>(<args...>)
괄호 (<statement>)
2 전위 증가와 감소 ++, --
단항 마이너스 -
단항 연산 delete
논리 NOT !
비트 NOT ~
3 지수 **
4 곱셈, 나눗셈과 모듈로 *, /, %
5 덧셈과 뺄셈 +, -
6 비트 시프트 연산자 <<, >>
7 비트 AND &
8 비트 XOR ^
9 비트 OR |
10 부등식 연산자 <, >, <=, >=
11 등식 연산자 ==, !=
12 논리 AND &&
13 논리 OR ||
14 삼항 연산자 <conditional> ? <if-true> : <if-false>
할당 연산자 =, |=, ^=, &=, <<=, >>=, +=, -=, *=, /=, %=
15 쉼표 연산자 ,

기본 타입 간의 변환 (Conversions between Elementary Types)

암시적 변환 (Implicit Conversions)

암시적 타입 변환은 할당 중, 함수에 인자를 전달할 때, 연산자를 적용할 때 일부 경우에 컴파일러가 자동으로 적용해요. 일반적으로 값 타입 사이의 암시적 변환은 의미상 타당하고 정보가 손실되지 않을 때 가능해요. 예를 들어 uint8은 uint16으로, int128은 int256으로 변환 가능하지만, int8은 uint256으로 변환 불가능해요. uint256은 -1 같은 값을 담을 수 없기 때문이에요.

다른 타입에 연산자가 적용되면 컴파일러는 피연산자 중 하나를 다른 타입으로 암시적으로 변환하려 해요(할당에도 동일). 즉 연산은 항상 피연산자 중 하나의 타입으로 수행돼요. 어떤 암시적 변환이 가능한지에 대한 자세한 내용은 타입 자체에 대한 섹션을 참고하세요.

아래 예에서 덧셈의 피연산자 y와 z는 같은 타입이 아니지만, uint8은 uint16으로 암시적으로 변환될 수 있고 그 반대는 안 돼요. 그 때문에 덧셈이 uint16 타입으로 수행되기 전에 y가 z의 타입으로 변환돼요. 표현식 y + z의 결과 타입은 uint16이에요. uint32 타입의 변수에 할당되므로 덧셈 후에 또 다른 암시적 변환이 수행돼요.

uint8 y;
uint16 z;
uint32 x = y + z;

명시적 변환 (Explicit Conversions)

컴파일러가 암시적 변환을 허용하지 않지만 당신이 변환이 작동할 것이라고 확신한다면, 명시적 타입 변환이 때로 가능해요. 이는 예상치 못한 동작을 초래할 수 있고 컴파일러의 일부 보안 기능을 우회할 수 있으므로, 결과가 원하고 예상하는 것인지 꼭 테스트하세요!

음수 int를 uint로 변환하는 다음 예를 보세요:

int  y = -3;
uint x = uint(y);

이 코드 조각이 끝나면 x는 0xfffff..fd(64개의 16진 문자) 값을 가지게 되는데, 이는 256비트의 2의 보수 표현에서 -3이에요.

정수가 더 작은 타입으로 명시적으로 변환되면 높은 차수의 비트가 잘려요:

uint32 a = 0x12345678;
uint16 b = uint16(a); // b will be 0x5678 now

정수가 더 큰 타입으로 명시적으로 변환되면 왼쪽(즉 높은 차수 끝)으로 패딩돼요. 변환 결과는 원래 정수와 비교해 같을 거예요:

uint16 a = 0x1234;
uint32 b = uint32(a); // b will be 0x00001234 now
assert(a == b);

고정 크기 바이트 타입은 변환 중에 다르게 동작해요. 이것들을 개별 바이트의 시퀀스로 생각할 수 있고, 더 작은 타입으로 변환하면 시퀀스가 잘려요:

bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b will be 0x12

고정 크기 바이트 타입이 더 큰 타입으로 명시적으로 변환되면 오른쪽으로 패딩돼요. 고정 인덱스의 바이트에 접근하면 변환 전후에 (인덱스가 여전히 범위 안이라면) 같은 값이 결과로 나와요:

bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b will be 0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);

정수와 고정 크기 바이트 배열이 잘리거나 패딩될 때 다르게 동작하므로, 정수와 고정 크기 바이트 배열 사이의 명시적 변환은 둘 다 같은 크기일 때만 허용돼요. 다른 크기의 정수와 고정 크기 바이트 배열 사이를 변환하려면 원하는 잘림과 패딩 규칙을 명시적으로 만드는 중간 변환을 사용해야 해요:

bytes2 a = 0x1234;
uint32 b = uint16(a); // b will be 0x00001234
uint32 c = uint32(bytes4(a)); // c will be 0x12340000
uint8 d = uint8(uint16(a)); // d will be 0x34
uint8 e = uint8(bytes1(a)); // e will be 0x12

bytes 배열과 bytes calldata 슬라이스는 고정 바이트 타입(bytes1 /…/ bytes32)으로 명시적으로 변환될 수 있어요. 배열이 대상 고정 바이트 타입보다 길면 끝에서 잘림이 발생해요. 배열이 대상 타입보다 짧으면 끝에 0으로 패딩돼요.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.5;

contract C {
    bytes s = "abcdefgh";
    function f(bytes calldata c, bytes memory m) public view returns (bytes16, bytes3) {
        require(c.length == 16, "");
        bytes16 b = bytes16(m);  // if length of m is greater than 16, truncation will happen
        b = bytes16(s);  // padded on the right, so result is "abcdefgh\0\0\0\0\0\0\0\0"
        bytes3 b1 = bytes3(s); // truncated, b1 equals to "abc"
        b = bytes16(c[:8]);  // also padded with zeros
        return (b, b1);
    }
}

리터럴과 기본 타입 간의 변환 (Conversions between Literals and Elementary Types)

정수 타입 (Integer Types)

십진 및 16진 숫자 리터럴은 잘림 없이 표현할 만큼 충분히 큰 어떤 정수 타입으로든 암시적으로 변환될 수 있어요:

uint8 a = 12; // fine
uint32 b = 1234; // fine
uint16 c = 0x123456; // fails, since it would have to truncate to 0x3456

참고: 0.8.0 버전 이전에는 어떤 십진이나 16진 숫자 리터럴도 정수 타입으로 명시적으로 변환될 수 있었어요. 0.8.0부터 그러한 명시적 변환은 암시적 변환만큼 엄격해요. 즉 리터럴이 결과 범위에 들어맞을 때만 허용돼요.

고정 크기 바이트 배열 (Fixed-Size Byte Arrays)

십진 숫자 리터럴은 고정 크기 바이트 배열로 암시적으로 변환될 수 없어요. 16진 숫자 리터럴은 변환될 수 있지만, 16진 자릿수가 바이트 타입의 크기에 정확히 맞을 때만이에요. 예외로 값이 0인 십진과 16진 리터럴 모두 어떤 고정 크기 바이트 타입으로든 변환될 수 있어요:

bytes2 a = 54321; // not allowed
bytes2 b = 0x12; // not allowed
bytes2 c = 0x123; // not allowed
bytes2 d = 0x1234; // fine
bytes2 e = 0x0012; // fine
bytes4 f = 0; // fine
bytes4 g = 0x0; // fine

문자열 리터럴과 16진 문자열 리터럴은 문자의 수가 바이트 타입의 크기와 같거나 작으면 고정 크기 바이트 배열로 암시적으로 변환될 수 있어요:

bytes2 a = hex"1234"; // fine
bytes2 b = "xy"; // fine
bytes2 c = hex"12"; // fine
bytes2 e = "x"; // fine
bytes2 f = "xyz"; // not allowed

주소 (Addresses)

주소 리터럴에서 설명한 대로, 체크섬 테스트를 통과하는 올바른 크기의 16진 리터럴은 address 타입이에요. 다른 리터럴은 address 타입으로 암시적으로 변환될 수 없어요. address로의 명시적 변환은 bytes20과 uint160에서만 허용돼요. 주소 a는 payable(a)를 통해 address payable로 명시적으로 변환될 수 있어요.

참고: 0.8.0 버전 이전에는 어떤 정수 타입(어떤 크기든, 부호 있든 없든)에서 address나 address payable로 명시적으로 변환하는 것이 가능했어요. 0.8.0부터는 uint160에서의 변환만 허용돼요.

더 알아보기 (Learn more)