CZ 002 - 주소와 매핑, 인터페이스 기초
address
주소는 특정 계정을 가리키는 고유 식별자로, 계좌번호와 같은 역할을 한다.
자료형이다.
0x0cE446255506E92DF41614C46F1d6df9Cc969183
Mapping(매핑)
매핑은 key-value 형태를 가지는 자료형이다.
아래와 같이 mapping을 정의할 수 있다.
// 금융 앱용으로, 유저의 계좌 잔액을 보유하는 uint를 저장한다:
mapping (address => uint) public accountBalance;
// 혹은 userID로 유저 이름을 저장/검색하는 데 매핑을 쓸 수도 있다
mapping (uint => string) userIdToName;
msg.sender
msg.sender는 전역 변수 중 하나로, 현재 함수를 호출한 사람(혹은 스마트 컨트랙트)의 주소를 담고 있다.
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
// `msg.sender`에 대해 `_myNumber`가 저장되도록 `favoriteNumber` 매핑을 업데이트한다 `
favoriteNumber[msg.sender] = _myNumber;
// ^ 데이터를 저장하는 구문은 배열로 데이터를 저장할 떄와 동일하다
}
function whatIsMyNumber() public view returns (uint) {
// sender의 주소에 저장된 값을 불러온다
// sender가 `setMyNumber`을 아직 호출하지 않았다면 반환값은 `0`이 될 것이다
return favoriteNumber[msg.sender];
}
require
rerquire는 참이 아닐 경우 에러를 던진다.
따로 괄호로 감싸지는 않고, 그냥 require 아래 작성된 코드가 참일 때 실행되는 코드이다.
function sayHiToVitalik(string _name) public returns (string) {
// _name이 "Vitalik"인지 비교한다. 참이 아닐 경우 에러 메시지를 발생하고 함수를 벗어난다
// (참고: 솔리디티는 고유의 스트링 비교 기능을 가지고 있지 않기 때문에
// 스트링의 keccak256 해시값을 비교하여 스트링 값이 같은지 판단한다)
require(keccak256(_name) == keccak256("Vitalik"));
// 참이면 함수 실행을 진행한다:
return "Hi!";
}
상속(is)
상속은 is
문으로 사용할 수 있다.
기본적으로 contract는 다른 언어의 class와 동일하다고 보면 된다.
즉, contract를 상속할 수 있다.
동일한 로직의 다수의 클래스로 분할해서 단순히 코드를 정리할 때도 사용하기도 한다.
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}
import
다른 파일을 불러오고 싶다면 import를 활용하여 불러올 수 있다.
(Solidity의 파일 확장자는 .sol
이다)
pragma solidity ^0.4.19;
import "./zombiefactory.sol"; // 같은 폴더의 다른 파일
contract ZombieFeeding is ZombieFactory {
}
Storage vs Memory
storage
는 하드디스크처럼 블록체인에 "영구적"으로 저장되는 값을 말하고,
memory
는 ram처럼 "임시로 저장"되었다가 사라지는 값을 의미한다.
solidity는 알아서 함수 외부에 선언된 상태 변수는 storage로 선언해 블록체인에 영구 저장되고,
함수 내에서 선언된 변수는 memory로 자동 선언되어 함수 호출이 종료되면 사라지기 때문에
보통은 이러한 값을 사용할 필요가 없다.
단, 함수 내의 구조체나 배열을 처리할 경우에 이 키워드를 사용해야한다.
contract SandwichFactory {
struct Sandwich {
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// ^ 꽤 간단해 보이나, 솔리디티는 여기서
// `storage`나 `memory`를 명시적으로 선언해야 한다는 경고 메시지를 발생한다.
// 그러므로 `storage` 키워드를 활용하여 다음과 같이 선언해야 한다:
Sandwich storage mySandwich = sandwiches[_index];
// ...이 경우, `mySandwich`는 저장된 `sandwiches[_index]`를 가리키는 포인터이다.
// 그리고
mySandwich.status = "Eaten!";
// ...이 코드는 블록체인 상에서 `sandwiches[_index]`을 영구적으로 변경한다.
// 단순히 복사를 하고자 한다면 `memory`를 이용하면 된다:
Sandwich memory anotherSandwich = sandwiches[_index + 1];
// ...이 경우, `anotherSandwich`는 단순히 메모리에 데이터를 복사하는 것이 된다.
// 그리고
anotherSandwich.status = "Eaten!";
// ...이 코드는 임시 변수인 `anotherSandwich`를 변경하는 것으로
// `sandwiches[_index + 1]`에는 아무런 영향을 끼치지 않는다. 그러나 다음과 같이 코드를 작성할 수 있다:
sandwiches[_index + 1] = anotherSandwich;
// ...이는 임시 변경한 내용을 블록체인 저장소에 저장하고자 하는 경우이다.
}
합수 접근 제어자 internal vs external
internal
은 private
과 동일하나 상속받은 contract에서 이 함수를 사용할 수 있다.(protected
).
반면 external
은 public
과 동일하나 contract 바깥에서만 사용할 수 있다.
contract Sandwich {
uint private sandwichesEaten = 0;
function eat() internal {
sandwichesEaten++;
}
}
contract BLT is Sandwich {
uint private baconSandwichesEaten = 0;
function eatWithBacon() public returns (string) {
baconSandwichesEaten++;
// eat 함수가 internal로 선언되었기 때문에 여기서 호출이 가능하다
eat();
}
}
인터페이스
다른 컨트랙트와 소통하기 위해 다른 컨트랙트에 선언된 함수를 작성해두는 것을 인터페이스라고 한다.
혼동하지 말아야할 것이, 컨트랙트를 호출하는 쪽에서 호출 당하는 쪽의 인터페이스를 작성해둬야한다.
호출 당하는 쪽이 작성해두는 것이 아니다.
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
위와 같은 LuckyNumber라는 컨트랙트가 있다고 하자.
내 앱 혹은 컨트랙트에서 LuckyNumber 컨트랙트에 접근해서 getNum을 실행하고 싶다면
내 컨트랙트 안에 아래와 같은 인터페이스를 정의 후 실행하여 상호작용 할 수 있다.
인터페이스를 선언할 때는 함수의 선언부만 작성하고 중괄호를 포함한 본체는 작성하지 않는다.
contract NumberInterface { // interface 선언부
function getNum(address _myAddress) public view returns (uint);
}
contract MyContract { // MyContract에서 호출 시 사용 방법.
address NumberInterfaceAddress = 0xab38...
// ^ 이더리움상의 FavoriteNumber 컨트랙트 주소이다
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress)
// 이제 `numberContract`는 다른 컨트랙트를 가리키고 있다.
function someFunction() public {
// 이제 `numberContract`가 가리키고 있는 컨트랙트에서 `getNum` 함수를 호출할 수 있다:
uint num = numberContract.getNum(msg.sender);
// ...그리고 여기서 `num`으로 무언가를 할 수 있다
}
}
다수의 반환값 처리
function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
function processMultipleReturns() external {
uint a;
uint b;
uint c;
// 다음과 같이 다수 값을 할당한다:
(a, b, c) = multipleReturns();
}
// 혹은 단 하나의 값에만 관심이 있을 경우:
function getLastReturnValue() external {
uint c;
// 다른 필드는 빈칸으로 놓기만 하면 된다:
(,,c) = multipleReturns();
}
if 문
JS의 if문과 동일하다
function eatBLT(string sandwich) public {
// 스트링 간의 동일 여부를 판단하기 위해 keccak256 해시 함수를 이용해야 한다는 것을 기억하자
if (keccak256(sandwich) == keccak256("BLT")) {
eat();
}
}