Rust Variables

이 포스트는 Rust를 처음 공부하면서 정리한 내용입니다. 해당 포스트는 개인적으로 기억하기 위한 메모 성격에 가깝습니다. “러스트 프로그래밍 공식 가이드 (2018, 제이펍)” 서적을 참고하였으며, 러스트 공식 사이트 에서 책과 동일한 내용을 찾을 수 있습니다. 따라서 더 자세한 내용을 찾으시면 위의 링크를 참고하시면 좋습니다.

변수의 가변성 (Mutability)

Rust는 기본적으로 값 변경이 불가능한 변수 타입을 가집니다. 이는 안정성을 높이고, 동시성이라는 장점을 활용할 수 있는 코드를 작성하도록 돕기 위해 Rust 언어적으로 제공하는 특징 중 하나입니다. 불변 변수는 선언문 이후에 값 변경을 시도하면 컴파일 타임에서 오류를 뱉습니다.

아래와 같은 코드는 오류를 만납니다.

fn main() {
    let x = 5;
    x = 10; // Impossible
    println!("The value of x is: {}", x);
}

mut

코드를 작성하다 보면 가변성이 필요한 경우가 생깁니다. 변수의 불변성은 기본 변수 선언문을 사용했을 때만 적용됩니다. 앞에 mut 키워드를 사용하면 가변 변수를 선언할 수 있습니다.

fn main() {
    let mut x = 5;
    x = 10; // No error!
    println!("The value of x is: {}", x);
}

const

변수의 값을 변경할 수 없다는 개념은 다른 프로그래밍 언어에서 지원하느 상수(Constant)의 개념과 닯아 있습니다. 그러나 Rust 에서는 따로 const 키워드를 지원하며 상수를 사용할 수 있습니다.

기본 불변 속성의 변수와 상수는 몇 가지 차이점이 존재하는데 이는 아래와 같습니다.

  1. let 대신 const 키워드를 사용합니다.
  2. 할당할 값의 타입을 반드시 지정해야 합니다.
  3. 전역 범위를 포함하여 어떤 범위 내에서도 선언이 가능합니다.
  4. 상수 표현식(expression)을 이용하여 값을 할당해야 하며 런타임에서 얻은 값은 사용할 수 없습니다.

다른 언어에서 지원하는 기능 중 많은 것들이 닮아 있어 쉽게 이해가 가능합니다.

const MAX_POINTS: u32 = 100_000;
// unsigned 32bit

변수 가리기 (Shadowing)

Rust에서는 이미 선언한 변수의 이름과 똑같은 이름의 변수를 선언할 수 있습니다. 이때 이전에 선언한 변수는 새로 선언한 변수 때문에 가려지게 됩니다.

변수의 이름을 참조하면 첫 번째 값 대신 두 번째 값이 사용되게 됩니다. 변수를 가리기 위해서는 같은 이름의 변수를 let을 이용해 다시 선언하면 됩니다.

fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;
    println!("The value of x is: {}", x);
    // 12 출력
}

여기서 확인 가능한 mut 가변 변수와의 차이점은 가려진 변수의 속성은 다시 불변 속성이 됩니다. 또한 새로 선언된 이름의 값은 다른 타입을 사용할 수도 있습니다.

let spaces = "   ";
let spaces = spaces.len();

데이터 타입 (Data Types)

데이터 타입은 데이터를 올바르게 처리하도록 종류를 명시한 것입니다. Rust 에서는 크게 두 가지의 데이터 타입을 지원합니다. Rust는 정적 타입 언어이므로 컴파일 타임에 모든 변수의 타입이 결정되어야 합니다. 컴파일러는 타입 추론을 통해 변수에 할당된 값이나 변수의 사용을 보고 실제 타입을 예측합니다. 그러나 여러 타입이 사용 가능한 경우 타입 애노테이션(annotation)을 통해 명시해주어야 합니다.

let guess: u32 = "42".parse().expect("Not a number!");

스칼라 타입 (Scalar Type)

스칼라 타입은 하나의 값을 표현합니다. Rust는 정수(integer), 부동 소수점 숫자(floating point numbers), 불리언(Boolean), 그리고 문자(characters) 네 가지 종류의 스칼라 타입을 정의하고 있습니다.

정수 (Integer Types)

Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

여기에서 isize와 usize 타입은 현재 프로그램이 실행 중인 아키텍처에 따라 크기가 달라집니다. 예를 들어 64 bit 환경이라면 이 타입은 64bit가 됩니다.

추가적으로 Rust의 정수 타입 기본 선택은 i32 입니다. (64 bit 환경에서도 마찬가지입니다.)

정수 리터럴 (literal)은 아래 표와 같이 표현할 수 있습니다.

Number literals Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b’A’

부동 소수점 (Floating Point Types)

Rust에서는 최근 CPU에서는 f64가 f32만큼 빠르면서도 정확도가 더 높아 기본으로 f64 타입을 규정하고 있습니다.

불리언 (Boolean)

대부분의 프로그래밍 언어와 마찬가지로 불리언 타입을 지원합니다. 불리언의 크기는 1byte 입니다.

fn main() {
    let t = true;
    let f: bool = false; // with explicit type annotation
}

문자 (Character Type)

Rust 에서의 char 타입은 4 바이트의 유니코드 스칼라값입니다. 따라서 ASCII 보다 더 많은 문자가 표현이 가능합니다. 한국어도 지원하고 이모지(Emoji)와 폭이 없는 공백 (Zero-width) 등 모든 문자가 유효합니다. 유니코드 스칼라 값의 범위는 U+0000부터 U+D7FF, 그리고 U+E000 부터 U+10FFFF 까지의 값을 포함합니다.

fn main() {
    let c = 'z';
    ...
}

컴파운드 타입 (Compound Type)

컴파운드 타입 (Compound Type)은 하나의 타입으로 여러 개의 값을 그룹화 한 타입입니다. Rust는 기본적으로 튜플(Tuple)과 배열(Array) 두 가지의 컴파운드 타입을 지원합니다.

튜플 (Tuple)

튜플은 고정된 길이를 가지고 한 번 정의하면 그 크기를 키우거나 줄일 수 없습니다. 튜플을 정의할 때에는 아래와 같이 괄호 안에 값의 목록을 쉼표로 구분해서 표기하면 됩니다. 이 때 튜플의 각 요소는 타입을 가지고 반드시 같을 필요는 없습니다.

아래와 같이 선택적으로 타입 애노테이션을 적용할 수도 있습니다.

fn main() {
    let tuple_var: (i32, f32, u8) = (1, 1.2, 5);
}

튜플은 하나의 컴파운드 요소로 인식이 되기 때문에 개별 값을 읽는 경우 패턴 매칭을 사용하여 해체(destruct)할 수 있습니다. 또한 요소의 인덱스를 지정하여 튜플의 각 요소를 직접 참조할 수도 있습니다.

fn main() {
    let tup = (500, 6.4, 1);
    let (x, y, z) = tup;

    println!("The value of y is: {}", y);

    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}

배열 (Array)

배열은 여러 개의 값의 컬렉션입니다. 배열은 튜플과 달리 모든 요소는 동일한 타입을 가지고 있습니다. Rust에서 배열은 대괄호(Square Bracket) 안에 쉼표로 구분해서 나열합니다.

fn main() {
    let arr = [1, 2, 3, 4, 5];
    let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

    let first = arr[0];
    let second = arr[1];
}

또는 아래와 같이 선언할 수도 있습니다. [type; number] 형식으로 타입 애노테이션을 사용하여 선언하는 경우는 다음과 같습니다.

fn main() {
    let a: [i32; 5] = [1, 2, 3, 4, 5];
}

Rust는 유효하지 않은 배열 요소에 접근하는 경우 panic 을 발생시킵니다. 컴파일은 정상적으로 진행되지만, 런타임 에러가 발생합니다. Rust는 배열의 인덱스를 이용해 요소를 읽을 때 지정된 인덱스가 배열의 전체 길이보다 작은 값인지 우선 검사합니다.

Rust는 안정성을 위해 다양한 장치를 가지고 있는데 이 panic이 대표적인 예 입니다. Rust는 엉뚱한 메모리에 대한 접근은 허용하지만 프로그램을 즉각 중단시키는 보호 장치를 제공하고 있습니다.

구조체 (Structs)

구조체(structs)는 튜플과 마찬가지로 다른 타입의 값으로 구성됩니다. 그러나 튜플과는 다른점이 있다면 각 데이터에 별개의 이름을 부여해서 의미를 더 분형히 표현이 가능합니다. 이때의 이름을 필드(Field)라고 합니다. 구조체의 정의와 인스턴스는 아래와 같이 생성합니다. 만일 인스턴스가 가변이라면 직접 필드를 수정할 수 있습니다.

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool
}

let user1 = User {
    username: String::from("someone"),
    email: String::from("sample.addr@example.com"),
    sign_in_count: 1,
    active: true
};

// 가변 인스턴스의 필드 값 수정
let mut user2 = User {
    username: String::from("someone2"),
    email: String::from("sample.addr@example.com"),
    sign_in_count: 1,
    active: true
};

user2.email = String::from("sample.addr2@example.com");

이때 주의할 점은 구조체 인스턴스 자체가 가변이라는 점 입니다. 일부 필드만 가변 데이터로 지정하는 것을 Rust는 지원하지 않습니다.

예제 1. 함수로부터 인스턴스 생성하기

아래의 예시는 함수로부터 인스턴스를 생성하는 방법입니다.

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

이때 반복되는 필드 이름을 피하기 위해서 아래와 같은 문법도 지원합니다.

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

예제 2. 기존 인스턴스로부터 생성하기

기존에 만들어 두었던 인스턴스에서 몇 가지 필드의 값만 수정한 상태의 새로운 인스턴스가 필요할 경우가 종종 있습니다. Rust는 구조체 갱신 문법(Struct Update Syntax)를 지원하기 때문에 쉽게 새로운 인스턴스를 생성할 수 있습니다.

let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

위의 예시는 email과 username 필드에 새로운 값을 대입하지만 그 외의 값은 user1의 인스턴스가 가진 값을 대입합니다.

예제 3. 이름 없는 필드를 가진 튜플 구조체로 다른 타입 생성하기

튜플과 유사하게 생긴 구조체를 선언할 수도 있습니다. 이를 튜플 구조체(Tuple Structs)라고 하는데, 구조체에게는 이름을 부여하지만 필드에는 이름을 부여하지 않고 타입만 지정하는 경우입니다. 일반적인 구조체와 달리 필드 이름이 불필요하거나 귀찮을 때 사용하며 다른 튜플들과는 다른 타입으로 사용하고자 할 때 유용합니다.

아래의 예시에서 Color와 Point는 동일한 타입을 가지고 있지만 black과 origin은 다른 타입으로 인식됩니다. 이 둘은 서로 다른 튜플 구조체의 인스턴스이기 때문입니다. 따라서 Color 타입이 매개변수인 함수에게 Point 타입의 변수를 넘겨줄 수는 없습니다.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

이러한 몇 가지 특징만 제외한다면 동작과 사용법은 튜플과 동일합니다. 마침표와 숫자 인덱스로 각 필드에 접근이 가능하고, 대응하는 변수로 해체할 수도 있습니다.

구조체 메서드(Method)

메서드(Methods)는 함수와 유사합니다. 마치 다른 프로그래밍 언어에서 class 안의 멤버 함수를 정의하는 것과 같습니다.

메서드는 첫 번째 매개 변수가 항상 self 이어야 합니다. 메서드 정의는 아래와 같이 impl 키워드를 사용하여 정의합니다.

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

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

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

위의 간단한 예시에서 self를 참조하고 있습니다. 이 함수는 Rectangle 구조체의 컨텍스트(Context)안에 존재하기 때문에 필드에 대한 정보를 이용할 수 있습니다. 위의 예시는 단순히 값을 계산하고 리턴하는 메서드 이므로 불변 참조를 이용하고 있습니다. 값을 읽기만 하면 되기 때문입니다. 그러나 자기 자신의 필드 값을 바꾸어야 할 때는 self의 가변 인스턴스를 대여할 수 있습니다. 이러한 경우 &mut self를 사용해야 합니다.

연관 함수 (Associated Function)

impl 블록 안에는 self를 매개변수로 갖지 않는 다른 함수도 선언이 가능합니다. 이러한 함수들을 연관 함수라고 부릅니다. 이 함수들은 구조체 인스턴스를 직접 전달받지 않기 때문에 메서드가 아니라 함수입니다. 여태까지 사용했던 String::from() 함수가 대표적인 예시 입니다.

연관 함수는 구조체의 새로운 인스턴스를 생성하는 생성자(Constructor)를 구현할 때 자주 사용됩니다.

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

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

연관 함수를 호출하려면 :: 문법을 사용하면 됩니다. 예를 들어

let instance = Rectangle::square(2);

과 같이 코드 작성이 가능합니다.

태그:

카테고리:

업데이트: