본문 바로가기
IT/Rust 기초 완전 정복

Rust 변수, 상수 그리고 데이터 타입 이해하기 (3)

by 지식 발전소 2024. 4. 22.
728x90
반응형

1

 

변수와 상수

변수 상수는 프로그래밍에서 데이터를 저장하고 조작하는 기본 요소입니다. 이번에는 Rust 언어에서 변수와 상수를 어떻게 정의하고 사용하는지 알아보겠습니다. 변수와 상수를 잘 이해하는 것은 Rust 프로그래밍의 기초가 되므로 이 부분을 확실히 익히면 앞으로 Rust를 사용하는 데 큰 도움이 될 것입니다.

변수 (Variables)

변수는 프로그램에서 값을 저장하고 변경할 수 있는 메모리 공간입니다. Rust에서는 let 키워드를 사용하여 변수를 선언합니다.

let x = 5; // 불변 변수 (immutable variable)
let mut y = 10; // 가변 변수 (mutable variable)

위 예시에서 x는 불변 변수, y는 가변 변수입니다. 불변 변수는 한 번 값이 할당되면 그 값을 변경할 수 없습니다. 반면 가변 변수는 값을 변경할 수 있습니다.

let mut y = 10;
println!("y = {}", y); // y = 10

y = 20; // 가변 변수의 값 변경
println!("y = {}", y); // y = 20

위 코드에서는 y의 값을 10에서 20으로 변경했습니다. 만약 y가 불변 변수라면 에러가 발생할 것입니다.

변수 이름은 영문자, 숫자, 그리고 밑줄(_)로 구성할 수 있습니다. 단, 변수 이름의 첫 문자로는 숫자가 올 수 없습니다. 또한 Rust에서는 snake_case 스타일의 변수 이름 작성 관행을 따릅니다.

let my_variable = 42;
let _unused_variable = 0; // 앞에 '_'를 붙이면 사용하지 않는 변수임을 나타냄

변수는 타입 추론(type inference) 을 통해 값의 타입이 결정됩니다. 즉, 개발자가 명시적으로 변수의 타입을 지정할 필요가 없습니다. 하지만 필요에 따라 타입을 명시할 수도 있습니다.

let x: i32 = 5; // 명시적으로 타입을 지정 (32비트 정수형)

변수는 프로그램에서 데이터를 저장하고 조작하는 핵심 요소입니다. 불변 변수와 가변 변수의 차이, 타입 추론 등을 잘 이해하고 적절히 활용하는 것이 중요합니다.

상수 (Constants)

상수는 값이 한 번 정해지면 변경할 수 없는 불변 데이터입니다. Rust에서 상수는 const 키워드를 사용하여 정의합니다.

const PI: f32 = 3.14159; // 32비트 부동 소수점 타입
const MY_STRING: &str = "Hello, Rust!"; // 문자열 슬라이스 타입

상수는 대문자 스네이크 케이스(SCREAMING_SNAKE_CASE)로 이름을 지어야 합니다. 또한 상수는 반드시 명시적으로 타입을 지정해야 합니다.

상수는 반드시 컴파일 시점에 값이 결정되어야 합니다. 따라서 상수의 값은 컴파일 시간에 계산 가능해야 합니다.

const SECONDS_IN_DAY: u32 = 60 * 60 * 24; // 허용됨
// const USER_NAME: &str = "Alice"; // 컴파일 에러: 런타임에 결정되는 값

상수는 변경이 불가능하므로 프로그램 전반에 걸쳐 동일한 값을 유지해야 할 때 유용합니다. 예를 들어 물리 상수나 전역 설정 값 등을 정의할 때 상수를 사용할 수 있습니다.

변수와 마찬가지로 상수도 프로그래밍에서 중요한 역할을 합니다. 상수는 값의 불변성을 보장하며, 코드의 가독성과 유지보수성을 높여줍니다.

이번 소주제에서는 Rust의 변수와 상수에 대해 알아보았습니다. 변수로 데이터를 저장하고 조작할 수 있으며, 상수는 불변 데이터를 정의하는 데 사용됩니다. 이러한 기초 개념을 잘 이해하고 활용하는 것이 Rust 프로그래밍의 첫 걸음이 됩니다.

데이터 타입

프로그래밍에서 데이터 타입은 매우 중요한 개념입니다. 데이터 타입은 메모리에 저장되는 데이터의 종류와 형식을 정의합니다. Rust는 정적 타입 언어로, 컴파일 단계에서 데이터 타입이 검사됩니다. 이번에는 Rust에서 지원하는 다양한 데이터 타입에 대해 알아보겠습니다.

스칼라 타입 (Scalar Types)

스칼라 타입은 단일 값을 나타내는 가장 기본적인 데이터 타입입니다. Rust에서 제공하는 스칼라 타입에는 정수형, 부동 소수점 숫자형, 불린형, 문자형이 있습니다.

정수형 (Integer Types)

정수형은 정수 값을 표현하는 데 사용됩니다. Rust에는 부호 있는(signed) 정수형과 부호 없는(unsigned) 정수형이 있습니다. 또한 정수의 비트 크기에 따라 여러 가지 타입이 존재합니다.

  • i8, i16, i32, i64, i128: 부호 있는 정수형 (8, 16, 32, 64, 128비트)
  • u8, u16, u32, u64, u128: 부호 없는 정수형 (8, 16, 32, 64, 128비트)

일반적으로 i32와 u32가 가장 많이 사용됩니다. 정수형 리터럴은 10진수, 16진수, 8진수, 2진수, 바이트 값으로 표현할 수 있습니다.

let x = 42; // 10진수 (기본)
let y = 0x2A; // 16진수
let z = 0o52; // 8진수
let binary = 0b101010; // 2진수
let byte = b'A'; // 바이트 값

정수형에는 오버플로와 언더플로 개념이 있습니다. 정수 값이 표현할 수 있는 범위를 벗어나면 오버플로나 언더플로가 발생합니다. Rust에서는 이를 디버깅 모드에서 검사하고, 릴리즈 모드에서는 값을 래핑(wrap around)합니다.

부동 소수점 숫자형 (Floating-Point Number Types)

부동 소수점 숫자형은 실수 값을 표현하는 데 사용됩니다. Rust에서는 IEEE 754 표준을 따르는 f32와 f64 타입을 제공합니다.

let x = 3.14; // f64 (기본)
let y: f32 = 3.14; // f32

부동 소수점 숫자형에는 특수한 값인 NaN(Not a Number)과 Infinity(무한대)가 있습니다. 또한 IEEE 754 표준에서 정의한 특별한 값들도 존재합니다.

let nan = f32::NAN; // NaN
let infinity = f64::INFINITY; // 무한대

부동 소수점 숫자형은 정확한 계산이 필요할 때는 적합하지 않을 수 있습니다. 예를 들어 0.1을 반복해서 더하면 정확한 값을 얻지 못할 수 있습니다. 이는 부동 소수점 숫자의 근본적인 한계입니다.

불린형 (Boolean Type)

불린형은 true와 false 두 가지 값만을 가질 수 있는 타입입니다. 주로 조건문이나 논리 연산에 사용됩니다.

let is_true = true;
let is_false = false;

문자형 (Character Type)

문자형은 유니코드 스칼라 값을 저장하는 데 사용됩니다. 문자형 리터럴은 작은따옴표(')로 묶어 표현합니다.

let c = 'a';
let smiley = '��'; // 유니코드 문자도 가능

문자열은 여러 개의 문자형으로 이루어진 컬렉션입니다. 문자열에 대해서는 뒤에서 다시 살펴보겠습니다.

복합 타입 (Compound Types)

복합 타입은 여러 개의 값을 그룹화한 타입입니다. Rust에서는 배열과 튜플과 같은 복합 타입을 제공합니다.

배열 (Arrays)

배열은 같은 타입의 값들을 순서대로 저장하는 컬렉션입니다. 배열의 길이는 고정되어 있으며, 컴파일 시간에 결정됩니다.

let numbers = [1, 2, 3, 4, 5]; // i32 타입의 배열
let fruits: [&str; 3] = ["Apple", "Banana", "Orange"]; // 문자열 슬라이스 배열

배열은 인덱스를 통해 요소에 접근할 수 있습니다. 단, 인덱스가 범위를 벗어나면 프로그램이 종료됩니다.

println!("First number: {}", numbers[0]); // 1
println!("Last fruit: {}", fruits[2]); // Orange

배열의 길이는 .len() 메서드로 확인할 수 있습니다.

println!("Length of numbers: {}", numbers.len()); // 5

튜플 (Tuples)

튜플은 서로 다른 타입의 값들을 하나의 복합 타입으로 그룹화할 수 있게 해줍니다. 튜플은 괄호(())로 묶어 표현합니다.

let person = ("John", 32, 1.75); // 문자열, 정수, 부동 소수점 숫자
let unit = (1, ); // 단일 요소 튜플은 끝에 쉼표를 붙여야 함

튜플의 요소에는 인덱스로 접근할 수 있습니다.

println!("Name: {}", person.0); // John
println!("Age: {}", person.1); // 32
println!("Height: {}", person.2); // 1.75

튜플은 구조체(struct)와 달리 요소의 의미를 명시할 수 없습니다. 따라서 작은 규모의 데이터를 다루거나 임시 변수를 만들 때 유용합니다.

기타 타입

위에서 살펴본 스칼라 타입과 복합 타입 외에도 Rust에서는 다양한 타입을 제공합니다.

  • 슬라이스(slice) 타입: 배열의 일부 연속된 요소들을 참조하는 타입
  • 문자열(str) 타입: UTF-8 인코딩 문자열 슬라이스
  • 구조체(struct) 타입: 사용자 정의 데이터 타입으로, 관련 데이터를 하나로 묶을 수 있음
  • 열거형(enum) 타입: 서로 관련된 여러 가지 변종을 정의할 수 있는 타입
  • 유니온(union) 타입: 여러 가지 데이터 타입을 하나의 메모리 공간에 저장할 수 있는 타입
  • 포인터 타입: 메모리 주소를 가리키는 타입

데이터 타입

문자열 타입

Rust에서 문자열은 매우 중요한 데이터 타입입니다. 문자열을 다루는 방법을 잘 익히는 것이 프로그래밍 실력 향상에 큰 도움이 될 것입니다.

Rust에는 str 타입과 String 타입, 두 가지 문자열 타입이 있습니다. 이 둘의 차이를 잘 이해해야 합니다.

str 타입

str은 불변 문자열 슬라이스를 나타내는 타입입니다. 문자열 리터럴이 이 타입에 해당합니다.

let hello: &str = "Hello, world!";

위 코드에서 hello는 &str 타입, 즉 문자열 슬라이스를 가리키는 참조자입니다. 문자열 슬라이스는 특정 메모리 영역의 UTF-8 바이트 시퀀스를 참조합니다.

str은 불변이므로 아래와 같이 직접 변경할 수 없습니다.

hello.push('!'); // 컴파일 에러

대신 슬라이스 연산을 사용하여 새로운 str을 만들어야 합니다.

let new_hello = &hello[0..5]; // "Hello"
let also_hello = "Hello, "; // &str
let hello_world = [also_hello, "world!"].concat(); // "Hello, world!"

String 타입

String은 가변 문자열을 나타내는 타입입니다. 내부적으로 Vec<u8>을 사용하여 문자열 데이터를 저장하고 관리합니다.

let mut hello = String::from("Hello, ");
hello.push_str("world!"); // "Hello, world!"

위 코드에서 hello는 String 타입이며, 문자열을 직접 변경할 수 있습니다. push_str 메서드로 새로운 문자열을 이어붙일 수 있습니다.

String은 힙에 할당되므로 메모리 관리가 필요합니다. 따라서 String을 다룰 때는 소유권(Ownership) 개념을 잘 이해해야 합니다.

let hello = String::from("Hello, "); // hello는 힙 메모리 영역을 소유
let world = hello; // hello의 소유권이 world로 이전
// hello는 더 이상 유효하지 않음

위 코드에서 world가 hello의 소유권을 가져갔으므로, hello는 더 이상 유효하지 않습니다. 이를 방지하기 위해 .clone() 메서드로 String을 복사해야 합니다.

let hello = String::from("Hello, ");
let world = hello.clone(); // 복사본 생성
println!("{}, {}", hello, world); // "Hello, , Hello, "

String과 &str 간의 상호 변환도 가능합니다.

let hello: &str = "Hello, world!";
let string = hello.to_string(); // &str -> String

let owned: String = String::from("Hello, world!");
let slice: &str = &owned; // String -> &str

프로그램에서 문자열을 다뤄야 하는 상황이 많기 때문에 str과 String의 특성을 잘 이해하고 적절히 활용하는 것이 중요합니다.

참조자와 포인터

프로그램에서 메모리를 직접 다루려면 참조자와 포인터에 대한 이해가 필요합니다. Rust에서는 이를 매우 중시하며, 메모리 안전성을 보장하기 위한 여러 규칙이 있습니다.

참조자

참조자(Reference)는 메모리 주소를 가리키는 값입니다. 참조자를 사용하면 값을 직접 소유하지 않고도 접근할 수 있습니다.

let x = 5;
let y = &x; // y는 x를 가리키는 참조자

위 코드에서 y는 x를 가리키는 불변 참조자입니다. 참조자는 & 기호를 사용하여 만듭니다.

참조자는 불변일 수도 있고, 가변일 수도 있습니다. 가변 참조자는 &mut 기호를 사용하여 만듭니다.

let mut x = 5;
let y = &mut x; // y는 x를 가리키는 가변 참조자
*y = 10; // x의 값이 10으로 변경됨

참조자를 사용할 때는 참조 규칙(Reference Rules) 을 지켜야 합니다.

  1. 어떤 데이터에 대해 가변 참조자는 하나만 존재할 수 있다.
  2. 가변 참조자가 있는 동안에는 불변 참조자를 만들 수 없다.

이 규칙은 데이터 경합(Data Race) 을 방지하기 위함입니다. 만약 규칙을 위반하면 컴파일 에러가 발생합니다.

포인터

포인터(Pointer) 는 메모리 주소 값 그 자체를 나타내는 값입니다. Rust에서는 raw pointer라고 부릅니다.

let x = 5;
let ptr = &x as *const i32; // x의 메모리 주소를 가리키는 포인터

위 코드에서 ptr은 x의 메모리 주소를 가리키는 불변 포인터입니다. 가변 포인터는 *mut 기호를 사용합니다.

포인터는 매우 저수준의 기능이므로 안전하지 않은 코드를 작성할 수 있습니다. 따라서 Rust에서는 안전한 추상화(Safe Abstraction) 를 권장하며, 포인터 대신 스마트 포인터나 참조자를 사용하도록 유도합니다.

참조자와 포인터는 메모리를 직접 다루는 저수준 기능입니다. 이를 잘 이해하고 활용하면 효율적이고 안전한 프로그램을 작성할 수 있습니다. 단, 주의 깊게 다뤄야 하며 메모리 안전성 규칙을 반드시 지켜야 합니다.

옵션과 결과

Rust에서 Option과 Result 타입은 안전한 오류 처리를 위해 매우 중요한 역할을 합니다.

Option 타입

Option 타입은 값이 있거나 없거나 두 가지 경우를 나타내는 열거형(enum) 타입입니다.

enum Option<T> {
    Some(T),
    None,
}

Some(T)은 T 타입의 값이 있음을 나타내고, None은 값이 없음을 나타냅니다. 이렇게 Option을 사용하면 null 참조로 인한 오류를 미연에 방지할 수 있습니다.

let x: Option<i32> = Some(5);
let y: Option<i32> = None;

Option 타입의 값을 다루려면 match 식이나 안전한 연산을 사용해야 합니다.

match x {
    Some(value) => println!("Value: {}", value), // 5
    None => println!("No value"),
}

// 안전한 연산
let sum = x.unwrap_or(0) + y.unwrap_or(0); // 5 + 0 = 5

unwrap 메서드는 Option 값이 Some일 때 그 값을 반환하고, None일 때는 패닉을 발생시킵니다. 따라서 unwrap_or와 같은 안전한 메서드를 사용하는 것이 좋습니다.

Result 타입

Result 타입은 성공과 실패 두 가지 경우를 나타내는 열거형 타입입니다.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Ok(T)는 T 타입의 값이 성공적으로 반환되었음을 나타내고, Err(E)는 E 타입의 오류가 발생했음을 나타냅니다.

let ok: Result<i32, &str> = Ok(5);
let err: Result<i32, &str> = Err("An error occurred");

Result를 다룰 때도 match 식이나 안전한 메서드를 사용해야 합니다.

match ok {
    Ok(value) => println!("Value: {}", value), // 5
    Err(err) => println!("Error: {}", err),
}

// 안전한 연산
let value = ok.unwrap_or(0); // 5
let value = err.unwrap_or(0); // 0

Result 타입은 오류를 명시적으로 처리하도록 강제하여 안전한 프로그래밍을 가능하게 합니다.

Option과 Result는 Rust의 핵심 철학인 메모리 안전성과 밀접하게 관련되어 있습니다. 이러한 타입을 적절히 활용하면 버그를 방지하고 더 견고한 코드를 작성할 수 있습니다.

Rust는 앞서 살펴본 다양한 데이터 타입을 제공하며, 각 타입의 특성을 잘 이해하고 활용하는 것이 중요합니다. 본 장에서는 스칼라 타입, 복합 타입, 문자열 타입, 참조자와 포인터, Option과 Result 타입 등에 대해 알아보았습니다. 데이터 타입은 Rust 프로그래밍의 기본 요소이므로, 이 부분을 확실히 익히면 앞으로 Rust 코드를 작성하는 데 큰 도움이 될 것입니다.

참고 자료

  1. 스티브 클래인버그, "The Rust Programming Language", No Starch Press, 2018.
  2. Rust 공식 문서, "The Rust Programming Language", https://doc.rust-lang.org/book/
  3. Michael Sampson, "Rust in Action", Manning Publications, 2021.
  4. Jim Blandy, Jason Orendorff, Leonora Tindall, "Programming Rust: Fast, Safe Systems Development", O'Reilly Media, 2021.
  5. "The Rust Reference", Rust 언어 참조 문서, https://doc.rust-lang.org/reference/

 

 

한 고대 문서 이야기

여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오

gospel79.tistory.com

 

유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법

올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을

stock79.tistory.com

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90
반응형

댓글