본문 바로가기

3.구현/기타언어

Rust 배우기1 - 기본

들어가기

이전부터 Rust 언어를 들어었지만 관심을 가지기 시작한 시점은 마이크로소프트가 핵심 코드를 Rust로 작성한다는 소식을 알게되었을 때이다. 러스트가 시스템 프로그래밍에도 사용할 수 있는 언어이고 C++을 대체하고 있다고 한다. 평소에 C++에 관심이 있어서 이번 기회에 한번 Rust을 정리하고 갈려고한다. 이전에 Kotlin을 정리했을 보다 시간이 다소 걸리에 시간을 두고 접근해야 한다. 기본에서는 가급적 Rust 언어 자체에 집중하고 표준 라이브러리에 대해 가급적 다루지 않을려고 한다. 또한 포인터와 참조에 대해서 어느정도 이해하고 있다고 가정하고 최대한 간략하게 설명하고 가급적 예제 중심으로 기능을 설명하려고 한다.

작성자: ospace114@empal.com, http://ospace.tistory.com/

Rust란?

Rust는 C++ 대안으로 선택가능한 다중 패러다임 및 범용 프로그래밍 언어이다. 모질라에서 개발했고 현재 러스트 커뮤니티에서 개발되고 있다. 비슷한 목적으로 Go가 반년일찍 나왔지만 Go는 가비지 컬랙션으로 시스템 프로그래밍에는 적합하지는 않는다고 한다.

Rust 언어의 특징을 간단하게 정리하면 다음과 같다.

  • 안전하고 빠르고 병렬성에 초점
  • 메모리 관리에 중점
  • 함수형 언어의 타입 시스템
  • 인터페이스와 유사한 트레잇으로 다형성 지원
  • null 포인터를 언어차원에서 제거
  • 타입 모호함 방지

Rust를 간단히 살펴본 결과 메모리 관리가 매우 큰 부분으로 다가왔다. 추후에 다룰 소유권과 라이프타임 관리 부분이 C++과 매우 큰 차이점이다. 또한 에러가 발생할 수 있는 부분을 근본에서 차단하고 모든 에러를 완벽하게 관리한다라는 느낌이다.

위의 문장을 다르게 표현하면 개발 자유도가 떨어진다라고 할 수 있다.

Hello world!

먼저 가장 기본적인 hello world부터 시작해보자.

fn main() {
  println!("Hello world!");
}

먼저 보이는 부분이 “fn main() { }”이다. 딱 봐도 함수처럼 보인다. 함수를 선언하기 위한 키워드 fn과 함수명 및 인자가 들어갈 수 있는 괄호 및 몸체에 해당하는 중괄호가 있다. 프로그램이 시작하기 위한 main()가 있음을 알 수 있다.

println!()은 매크로(macro)를 호출하는 부분이다. 뒤에 느낌표가 오는 함수처럼 보이는 표현식이 매크로라고 한다. println!()은 화면 출력을 위한 매크로이다. 그리고 문자열이 큰따옴표(“)로 묶이고, 문장 마지막은 세미콜론(;)이 나온다.

println!()은 포메팅도 가능하다.

println!("one is {}", 1); // 위치 변수
println!("value is {:?}", (0,1)); /* 다양한 타입 출력 */

위의 예제에도 보이지만 C와 C++과 동일한 한줄 주석과 여러줄 주석도 사용할 수 있다.

가변/불변 변수

Rust에서는 상수와 변수가 아닌 가변 또는 불변 변수로 사용된다. 변수 표기할 때 “let 변수명:자료형” 형태로 표기할 수 있다. 값으로 초기화할 경우 “:자료형”은 생략가능하고 초기값에서 타입을 추론한다. 기본은 변경불가(imuutable)하며 변경하는(mutable) 변수로 선언하고 싶다면 mut 키워드를 사용해야 한다.

fn main() {
  let msg1 = "Hello, world!";
  println!("{}", msg1);

  let mut msg2 = "Hi!";
  msg2 = "Hello, rust!";
  println!("{}", msg2);

상수로 지정할 수 있는 const 키워드도 있다.

const MSG = "Hello world!";
println!("{}", MSG);

변수 선언할 때에는 shadowing이라는 기능으로 동일한 변수명으로 선언하게 되면 이전 변수가 가려지고 새로 선언된 변수가 사용된다. 변수명과 타입은 동일해야하며 불변과 가변은 상관없다. 만약 변수명은 같은데 타입이 달라지면 컴파일 에러가 발생한다.

fn main() {
  let msg = "Hello world!";
  {
    let msg = "Hi world!"; // 셰도잉
    println!("{}", msg);
  }
  println!("{}", msg); // 이전 변수로 복귀
}

기본 자료형

스칼라 타입

변수 선언할 때에 사용되는 자료형이다. 자료형 중에 스칼라 타입을 보자.

Length Signed Unsigned Value
8-bit i8 u8 2^7 ~ 2^7-1, 0 ~ 2^8-1
16-bit i16 u16 2^15 ~ 2^15-1, 0 ~ 2^16-1
32-bit i32 u32 2^31 ~ 2^31-1, 0 ~ 2^32-1
64-bit i64 u64 2^63 ~ 2^63-1, 0 ~ 2^64-1
128-bit i128 u128 -2^127 ~ 2^127-1, 0 ~ 2^128-1
32-bit f32
64-bit f64
bool true, false
fn main() {
  let v1:i32 = -10;
  let v2:u8 = 10;
  let v3:bool = true;
  println!("v1[{}] v2[{}] v3[{}]", v1, v2, v3);
}

값 표현을 위한 정수형 리터럴이다.

  • Decimal(자릿수): 123_456
  • Hex: 0xff
  • Octal: 0o77
  • Binary: 0b1111_0000
  • Byte: b’A’
fn main() {
  println!("decimal = {}", 123_456); // 123456
  println!("hex = {}", 0xff); // 255
  println!("octal = {}", 0o77); // 63
  println!("binary = {}", 0b1111_0000); // 240
  println!("byte = {}", b'A'); // 65
}

문자는 작은 따옴표(‘)로 묶어서 표현하며 문자열은 큰 따옴표(“)로 묶어서 표현한다. 문자의 자료형은 char를 사용한다. 그리고 특수 문자는 역슬래시()로 표현한다.

fn main() {
  let c1 = 'A';
  let c2: char = 'Z';
  let s = "R \x52\u{211D}";
  println!("c1[{}] c2[{}] s[{}]", c1, c2, s);
}

문자열을 다루는 자료형으로 String이 있다. 이는 기본 자료형이 아니고 표준 라이브러리에 포함되어서 나중에 더 자세히 다룰 예정이지만, 간단하게 다룰려고 한다.

fn main() {
  let mut s1 = String::from("Hello");
  s1.push_str(" world!"); // 문자열 추가
  let s2 = s1.clone();  // 복제
  println!("{} {}", s1, s2);
}

String::from()에 의해서 새로운 String 객체가 생성된다. 그리고 push_str()로 문자열을 추가하거나 clone()에 의해 새로운 문자열을 복제할 수 있다.

복합형(Compound) 타입

추가로 다룰 자료형으로 튜플과 배열이 있다. 별도로 분리해서 다루기 모호해서 여기에서 간단하게 다룰려고 한다.

튜플은 여러 타입을 중괄호(”()“)를 이용해 그룹화할 수 있다. 마침표(.)로 요소에 접근한다. 그리고 튜플을 사용해 해체할 수도 있다.

fn main() {
  let tp: (i32, f64) = (10, 0.1);
  let (x, y) = tp;
  println!("tp({}, {})", tp.0, tp.1);
  println!("x[{}] y[{}]", x, y);
}

배열은 통일한 타입을 대괄호(“[]”)을 이용해 그룹화한다. 대괄호(“[]”)로 요소에 접근한다. 그리고 배열을 이용해 해체할 수도 있다.

fn main() {
  let a1 = [1, 2, 3];
  println!("1st is {}, and len is {}", a1[0], a1.len());
  let [v1, v2, v3] = a1;
  println!("v1[{}] v2[{}] v3[{}]", v1, v2, v3);

  // 자료형 i32으로 3개 배열 선언하고 기본값 0으로 3개 채움
  let a2:[i32; 3] = [0; 3]; 

  for val in a2.iter() {
    println!("{}", val);
  }
}

제어문

if 문

if 문은 조건에 맞을 경우 실행한다. 만약 else가 있다면 맞지 않을 경우 실행되며 else if가 있다면 새로운 조건으로 다시 맞는지 확인해서 실행한다.

fn main() {
  let val = 1;
  if val > 0 {
    println!("positive");
  } else {
    println!("negative");
  }
}

if 문으로 조건할당 할 수 있다.

fn main() {
  let val = 1;
  let isPositive = if val > 0 {
    true
  } else {
    false
  };
  println!("isPositive[{}]", isPositive);
}

뒤에 세미콜론 없이 표현식을 쓰면 리턴값이 된다.

loop 문

loop 문은 조건문 없이 무한반복한다. 중간에 종료하고 싶다면 프로그램을 종료하거나 조건문으로 break을 호출해야 한다.

fn main() {
  let mut cnt = 0;
  loop {
    cnt += 1;
    if 0 == cnt % 2 {
      println!("{} is even", cnt);
      continue;
    }
    if 10 < cnt { break; }
  }
}

loop문도 중간 결과를 리턴할 수 있다.

fn main() {
    let mut cnt = 0;
  let res = loop {
    cnt += 1;
    if cnt == 10 {
      break cnt; // 결과리턴
    }
  };
  println!("cnt is {}.", cnt);
}

loop 문에 라벨 사용해보자. 라벨이름은 앞에 작은 따옴표가 들어간 “’라벨이름“으로 표기한다.

fn main() {
  'outer: loop {
    println!("outer loop.");
    'inner: loop {
        println!("inner loop.");
        break 'outer; // 라벨 outer loop를 빠져나감
    }
   }
}

while 문

while 문은 조건이 있는 반복이다.

fn main() {
  let mut cnt = 3;
  while cnt > 0 {
    println!("Hello!");
    cnt -= 1;
  }
}

for 문

for 문에서 범위는 “..”을 사용해서 지정할 수 있다. 주의할 부분은 마지막 값은 포함되지 않는다. 만약 마지막 값을 포함하려면 “..=”를 사용한다. 전자를 배타적 범위(exclusive range)이라고 하고 후자를 포괄 범위(inclusive range)라고 한다. rev()를 사용해 역순으로도 가능하다. 또한 나중에 다룰 Iterator를 획득해서도 순회할 수있다.

fn main() {
  for i in 1..5 {
    println!("{}", i);
  }
  for i in 1..=5 {
    println!("{}", i);
  }
  for i in (1..5).rev() {
    println!("{}", i);
  }
}

match 문

C 언어의 switch와 비슷하다. 패턴 매칭을 지원한다.

fn main() {
  let val1 = 3;
  match val1 {
    1 => println!("one"),
    2 | 3 | 5 | 7 | 11 => println!("prime"),
    10..=19 => println!("teen"),
    _ => println!("normal"),
  }
  let val2 = (true, "hello");
  match val2 {
    (true, m) => println!("(true, {:?})", m),
    (false, ..) => println!("false"),
  }
  let val3 = [1,2,3];
  match val3 {
    [0, v2, v3] => println!("zero, {}, {}", v2, v3),
    [1, _, v3] => println!("one, ignore, {}", v3),
    [2, v2, ..] => println!("two, {}, others", v2),
    [3, v2, tail@..] => println!("three, {}, {:?}", v2, tail),
    [v1, v2@.., v3] => println!("{}, {:?}, {}", v1, v2, v3),
  }
}

패턴매칭에서 “_”은 임의 한개 값과 매칭되면 값은 바인드되지 않으며, “..”은 임의 여러 개값과 매칭되고 값은 바인드되지 않는다. 리터럴과 배열 외에도 enum과도 가능한데, 이는 enum에서 다루겠다.

함수

함수 정의

다음으로 함수를 살펴보자. 함수 정의는 “fn 함수명 (인자: 타입, ..)-> 리턴타입 { 함수 몸체 }“로 표현된다.

fn printNum(v: i32) {
  println!("{}", v);
}

fn inc(v: i32) -> i32 {
    v + 1
}

fn main() {
  let val = inc(1);
  printNum(val);
}

함수 리턴은 “return” 키워드를 사용할 수 있지만 마지막 세미콜론 없이 사용하면 자동으로 리턴값으로 인식한다. 이는 함수 뿐만 아니라 앞에서 if 문, match 문등 다양한 곳에서 사용된다. 이는 { } 안에 있는 문장의 종결 표현식이 세미콜론을 사용하지 않으면 반환값이 된다는 규칙이다.

고차함수

고차 함수는 함수를 인자로 받는 함수를 의미한다. 즉, 함수 포인터를 받아서 실행하는 함수를 의미한다.

fn calc(val: i32, op: fn(i32)->i32) -> i32 {
  op(val)
}

fn mul(val: i32) -> i32 {
  val * val
}

fn main() {
  let val = 10;
  println!("mul of val is {}", calc(val, mul));
}

함수를 받는 인자 타입은 “fn(인자타입1, …)->리턴타입“ 형태로 표현한다. 함수에 따라서 인자가 없을 수도 있고 리턴이 없을 수도 있다.

클로저

클로저는 실행하는 범위에서 변수를 캡처하는 익명함수이다. 클로저가 생성되는 시점에 접근할 수 있는 모든 범위에 변수가 해당된다. 클로저 정의는 “|인자:타입1, ..|→리턴타입 { 클로저 몸체 }” 형태로 표현된다. 인자에 타입을 생략할 수 있고, 리턴타입도 생략가능하며, 클로저 몸체에 표현식이 한줄이면 중괄호를 생략할 수 있다. 클로저도 함수에 리턴 규칙을 따른다. 이로 인해 클로저 표현식이 매우 단순해 진다.

fn calc(val: i32, op: fn(i32)->i32) -> i32 {
  op(val)
}

fn inc1(v: i32) -> i32 { v + 1 }
fn main() {
  let inc2 = |v: i32| -> i32 { v + 1 };
  let inc3 = |v|             { v + 1 };
  let inc4 = |v|               v + 1  ;
  let ten = || 10;
  let v0 = ten();
  let v1 = inc1(v0);
  let v2 = inc2(v1);
  let v3 = inc3(v2);
  let v4 = inc4(v3);
  println!("{} {} {} {} {}", v0, v1, v2, v3, v4);
  println!("mul of v0 is {}", calc(v0, |v| v*v));
}

또한 클로저를 초월 함수로도 넘길 수 있다. 한가지 주의할 부분은 인자타입이 없는 클로저를 호출할 경우 처음 호출해서 넘겨주는 인수에 의해서 타입이 결정된다. 이후로는 다른 타입을 사용할 수 없다.

let inc = |v| v + 1;
let v1: i32 = 0;
let v2: u32 = 0;
let r1 = inc(v1);
let r2 = inc(v2); // 에러

소유권(Ownership)

소유권은 가장 핵심 기능으로 반드시 이해하고 넘어가야 한다. 눈치 챈 분도 있을 지 모르기만 함수에 사용하는 모든 인자와 리턴 타입은 리터럴형이다. 이런 이유는 소유권으로 인한 부분이다. 소유권은 할당된 메모리에 대한 관리 권한을 의미한다.

RAII

Rust에서는 강제로 RAII(Resource Acquisitoin is Initialization)이 적용된다. 할당받은 범위에서 벗어나는 경우 강제로 자원은 해제된다. 이를 어기면 컴파일 에러가 발생한다. 대부분의 메모리 관리 필요한 부분은 힙이고 이에 대하 소유권을 관리한다. 소유권을 가지는 소유자(owner)은 단 하나만 존재한다. 복수로는 불가하다. 다른 변수로 할당하면 소유권은 이동되어서소 잃어버린다. 소유권을 잃어버리면 메모리 접근을 할 수 없게 된다. 간단한 예를 보자.

fn main() {
  let s1 = String::from("Hello world!");
  let s2 = s1; // 소유권이 s2로 이동
  println!("{} {}", s1, s2); // 컴파일 에러
}

s1의 소유권이 이동했기 때문에 더 이상 사용할 수 없다. 이를 해결할 가장 단순한 방법은 복제한다.

fn main() {
  let s1 = String::from("Hello world!");
  let s2 = s1.clone(); // 복제, 새로 메모리 할당
  println!("{} {}", s1, s2);
}

리터럴인 경우 기본적으로 복제되기 때문에 소유권을 신경쓸 필요가 없다.

fn main() {
  let v1 = 10;
  let v2 = v1;
  println!("{} {}", v1, v2);
}

소유권 이전은 최대한 이해하기 쉽게하기위해서 단순 할당으로 설명했지만 함수 호출이나 구조체 생성 등 다양한 곳에서 적용된다.

불변 참조자

복제는 같은 데이터 중복되기에 많은 메모리가 소비된다. 이를 해결할 다른 방법은 참조이다. 참조자를 만들면 기존 메모리를 참조할 수 있게된다. 참조자 생성은 엠퍼센드(&) 기호를 사용한다.

fn main() {
  let s1 = String::from("Hello world!");
  let s2 = &s1;
  println!("{} {}", s1, s2);
}

가변 참조자

앞의 참조자를 불변 참조자로 읽기 전용이다. 값을 변경할 경우 가변 참조자를 생성해야 한다. 가변 참조자 생성은 mut가 추가된 “&mut” 표현식을 사용한다.

fn main() {
  let mut s1 = String::from("Hello");
  let s2 = &mut s1;
  s2.push_str(" world!");
  println!("{}", s1);
}

가변 참조자 사용에는 제약사항이 있다.

  • 스코프 내에 두개 이상 가변 참조자를 생성해서 같이 사용 불가하고 최종 가변 참조자만 사용 가능
  • 가변 참조자가 참조하는 원본 인스탄스 사용시 가변 참조자 사용 불가

스코프 내에 두개 이상 가변 참조자를 생성해서 같이 사용할 수 없다.

let mut s = String::from("Hello");
let r1 = &mut s;
let r2 = &mut s;
r1.push_str(" world"); // 에러
r2.push_str("!");
println!("{}", s);

마지막 생성된 가변참조자인 r2만 사용할 수 있다.

let mut s = String::from("Hello");
let r1 = &mut s;
let r2 = &mut s;
r2.push_str(" world!");
println!("{}", s);

가변 참조자가 참조하는 원본 인스탄스를 사용할 경우 가변 참조자 사용할 수 없다.

let mut s = String::from("Hello");
let r1 = &mut s;
println!("{}", s); // 에러
r1.push_str(" world!");
println!("{}", s);

이를 수정하려면 원본 인스탄스 사용하려면 가변 참조자 생성 전에 사용하거나, 가변 참조자 사용이 끝난 후에 사용하면 된다.

let mut s = String::from("Hello");
println!("{}", s);
let r1 = &mut s;
r1.push_str(" world!");
println!("{}", s);

댕글링 포인터

댕글링 포인터(Dangling pointer)는 다른 곳에서 포인터를 가진 상태에서 포인터가 해제된 경우 잘못된 포인터를 참조하는 포인터를 말한다. Rust에서는 이런 댕글링 포인터를 가지지 않도록 보장한다. 아래 코드를 보면 리턴값이 댕글링 포인터가 된다.

fn foo() -> &String {
  let ret = String::from("Hi");
  &ret
}
fn main() {
  println!("{}", foo());
}

foo()가 종료되면 ret에 할당된 메모리는 해제되기에 이를 참조하는 참조자는 댕글링 포인터가 되면서 컴파일 에러가 발생한다. 이런 경우는 참조가 아닌 직접 String을 반환하면 된다. 자연스럽게 소유권 이동으로 객체 복사가 생기지 않는다.

fn safeFoo() -> String {
  let ret = String::from("Hi");
  ret
}

슬라이스(Slices)

슬라이스는 포인터에 할당된 데이터 범위에서 특정 지점을 가리키는 정보이다. 정보는 포인터와 길이가 있다. 슬라이스 예로 String에 스트링 슬라이스가 있다. 번역한다면 문자열 조각이라고 할 수 있다. 스크링 슬라이스 예를 보자.

fn main() {
  let s = String::from("Hello world!");
  let hello1 = &s[0..5]; // 스트링 슬라이스
  let hello2 = &s[..5]; // 스트링 슬라이스
  println!("{} {}", hello1, hello2);

  let len = s.len();
  let world1 = &s[6..len];
  let world2 = &s[6..];
  println!("{} {}", world1, world2);
}

만약 s 문자열을 해제한다면 스트링 슬라이스인 hello는 잘못된 위치를 참조할 수 있기 때문에 컴파일 에러가 발생한다.

let s = String::from("Hello world!");
let hello = &s[0..5];
s.clear(); // 에러
println!("{}", hello);

이런 스트링 슬라이스을 나타내는 타입으로 &str을 사용한다.

fn hello(s: &String) -> &str {
  &s[0..5]
}

fn main() {
  let s = String::from("Hello world!");
  println!("{}", hello(&s));
}

사용자 정의 자료형

사용자 정의 자료형으로는 struct와 enum이 있다.

구조체(Structures)

구조체는 튜플 구조체, C 구조체, 단위 구조체로 3가지가 있다.

// 튜플 구조체
struct Point(f32, f32);

// C 구조체
struct Rectangle {
  width: u32,
  height: u32,
}

// 단위 구조체
struct Unit;

fn main() {
  // 튜플 구조체
  let p = Point(0.1, 0.2);
  println!("p({}, {})", p.0, p.1);
  let Point(x, y) = p; // 구조체 해체
  println!("p({}, {})", x, y);

  // C 구조체
  let width = 11;
  let rect1 = Rectangle { width, height: 10 };
  println!("rect1[{}, {}]", rect1.width, rect1.height);
  // 구조체 갱신구문(struct update syntax)
  let rect2 = Rectangle { width:22, ..rect1 };
  println!("rect2[{}, {}]", rect2.width, rect2.height);
  let Rectangle {width, height:h} = rect2; // 구조체  해체
  println!("rect2[{}, {}]", width, h);

  // 단위 구조체
  let _unit = Unit; // 그냥 인스탄스 생성
}

열거형(Enums)

열거형을 구조체보다 늦게 다룬 이유가 있다. Rust에서 열거형은 특별하다. 기존 언어에서 열거형은 단순 데이터 나열로 각 데이터의 타입은 동일하고 고정되어 있다. Rust에서는 다양한 데이터 타입이 썩여서 포함될 수 있고, 값도 변경가능하다. 다양한 데이터 타입에는 튜플 뿐만 아니라 구조체도 포함된다. 이렇게 포함된 구성요소를 variant라고 한다. 다음 처럼 다양한 varient을 포함할 수있다.

enum Message {
  Quit, // 단위 구조체
  Move { x: i32, y: i32 }, // C형 구조체
  Write(String), // 튜플
}

다음은 단위 구조체와 튜플이 포함된 Color 열거형이다.

enum Color {
    Red,             // Unit-like
    Green,           // Unit-like
    Blue,            // Unit-like
    RGB(u8, u8, u8), // 튜플
}

fn main() {
  let color = Color::Red;

  match color {
      Color::Red => println!("red!"),
    Color::Green => println!("green!"),
    Color::Blue => println!("blue!"),
    Color::RGB(r,g,b) => println!("color: {}, {}, {}", r, g, b),
  }
}

match 문을 사용해 Color 열거형을 분기해서 처리할 수 있다. match 문외에 if set 문도 사용가능하다.

enum Color {
    Red,             // Unit-like
    Green,           // Unit-like
    Blue,            // Unit-like
    RGB(u8, u8, u8), // 튜플
}

fn main() {
  let color = Color::RGB(10,20,30);

  if let Color::RGB(r, g, b) = color {
    println!("RGB({}, {}, {})", r, g, b);
  }
}

if 문은 color가 Color 열거형의 RGB(..)으로 할당 가능한 경우 해당 값을 처리한다.

impl 키워드

impl 키워드를 사용해 구조체에 메소드를 추가할 수 있다. 메소드 형식은 “fn 메소드명(셀프, 인자1:타입, …) -> 리턴타입 { 메소드 몸체 }“으로 되어 있다. 실제 예를 보자.


struct Rectangle {
  width: u32,
  height: u32,
}

impl Rectangle {
  fn area(&self) -> u32 {
    self.width * self.height
  }
  fn square(size: u32) -> Rectangle {
    Rectangle { width: size, height: size }
  }
}

fn main() {
  let rect = Rectangle::square(10);
  println!("area = {}", rect.area());
  println!("area = {}", Rectangle::area(&rect));
}

셀프 부분은 구조체 자신 인스탄스을 의미한다. 셀프가 “self”인 경우는 소유권 이동이 된다. ”&self”은 불변 참조가 되며 “&mut self”은 가변 참조자가 된다. 셀프에는 별도 타입이 명시하지 않아도 자신이 된다. 호출하는 방식은 메소드 처럼 호출하거나 직접 호출해서 인스탄스를 인자로 넘길 수도 있다. 즉, 셀프가 없다면 외부에서 정적 메소드 처럼 호출할 수도 있다. 이런 메소드를 연관 함수(associated function)이라고 하고 호출은 “구조체이름::연관함수이름”으로 접근할 수 있다.

impl 키워드는 열거형에도 적용가능하다.

enum Color {
    Red,             // Unit-like
    Green,           // Unit-like
    Blue,            // Unit-like
    RGB(u8, u8, u8), // 튜플
}

impl Color {
  fn name(&self) -> &str {
    match self {
        Self::Red => "red",
        Self::Green => "green",
        Self::Blue => "blue",
        _ => "RGB",
    }
  }
}

fn main() {
  let color = Color::Red;
  println!("{}", color.name());
}

대문자 셀프인 Self은 자신 타입을 가리킨다.

마무리

지금까지 맛보기로 최대한 쉽고 아주 간단하게 Rust를 살펴보았다. 아직 중요한 부분은 다루지 않았다. 다름 심화 부분에 다룰려고 한다. 부족한 글이지만 여러분에게 도움이 되었으면 합니다. 즐거운 코딩생활 하세요. ospace.

참고

[1] Learn Rust, https://www.rust-lang.org/learn

[2] Rust(프로그래밍 언어), https://namu.wiki/w/Rust(프로그래밍 언어)

[3] Rust Playground, https://play.rust-lang.org/?version=stable&mode=debug&edition=2021

반응형

'3.구현 > 기타언어' 카테고리의 다른 글

Rust 배우기3 - 활용  (2) 2024.01.10
Rust 배우기2 - 심화  (2) 2024.01.06
C++개발자 위한 C샵 배우기  (0) 2007.10.22
[Flash] 3D Action Script 활용  (0) 2007.05.22