2025. 3. 16. 11:56ㆍ기타
https://doc.rust-kr.org/ch03-02-data-types.html
정수 오버플로우 (integer overflow) 가 일어나는데, 이는 둘 중 한 가지 동작을 일으킵니다. 코드를 디버그 모드에서 컴파일하는 경우, 러스트는 런타임에 정수 오버플로우가 발생했을 때 패닉 (panic) 을 발생시키는 검사를 포함시킵니다. 러스트에서는 에러가 발생하면서 프로그램이 종료되는 경우 패닉이라는 용어를 사용합니다
fn main() {
let t = true;
let f: bool = false; // 명시적인 타입 어노테이션
}
위와 같이 타입을 정확히 지정 가능
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
fn main() {
let a = [1, 2, 3, 4, 5];
}
복합 타입 가능(튜플), 배열 존재
함수
용법: snake case, 모든 글자를 소문자로 쓰고 밑줄 (underscore) 로 단어를 구분하는 방식
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
d
호출함수가 후에 옴
-> 러스트는 함수 위치 고려X
함수 시그니처에서는 각 매개변수의 타입을 반드시 선언
fn main() {
let x = (let y = 6);
}
let y = 6; 는 값을 반환 하지 않음, C와 Ruby의 차이점
고로 x에 bind 할 것이 없음
x + 1 줄의 마지막이 세미콜론으로 끝나지 않은 점, 표현식은 종결을 나타내는 세미콜론을 쓰지 않습니다. 만약 표현식 끝에 세미콜론을 추가하면, 표현식은 구문으로 변경되고 값을 반환하지 않게 됩니다.
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
#2번쨰 CASE
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
LOOP, WHILE, FOR 문 존재.
대부분의 프로그래밍 언어에서는 스택, 힙 영역을 주제로 고민할 필요가 많지 않습니다. 하지만 러스트 같은 시스템 프로그래밍 언어에서는 값을 스택에 저장하느냐 힙에 저장하느냐의 차이가 프로그램의 동작 및 프로그래머의 의사 결정에 훨씬 큰 영향을 미칩니다.
힙은 스택보다 복잡합니다. 데이터를 힙에 넣을 때 먼저 저장할 공간이 있는지 운영체제에 물어봅니다. 그러면 메모리 할당자는 커다란 힙 영역 안에서 어떤 빈 지점을 찾고, 이 지점은 사용 중이라고 표시한 뒤 해당 지점을 가리키는 포인터 (pointer) 를 우리한테 반환 -> 할당
포인터는 크기가 정해져 있어 스택에 저장
스택 영역은 데이터에 접근하는 방식상 힙 영역보다 속도가 빠릅니다. 메모리 할당자가 새로운 데이터를 저장할 공간을 찾을 필요가 없이 항상 스택의 가장 위에 데이터를 저장하면 되기 때문이죠. 반면에 힙에 공간을 할당하는 작업은 좀 더 많은 작업을 요구하는데, 메모리 할당자가 데이터를 저장하기 충분한 공간을 먼저 찾고 다음 할당을 위한 준비를 위해 예약을 수행해야 하기 때문입니다.
코드 어느 부분에서 힙의 어떤 데이터를 사용하는지 추적하고, 힙에서 중복되는 데이터를 최소화하고, 쓰지 않는 데이터를 힙에서 정리해 영역을 확보하는 등의 작업은 모두 소유권과 관련
소유권 규칙
- 러스트에서, 각각의 값은 소유자 (owner) 가 정해져 있습니다.
- 한 값의 소유자는 동시에 여럿 존재할 수 없습니다.
- 소유자가 스코프 밖으로 벗어날 때, 값은 버려집니다 (dropped).
{ // s는 아직 선언되지 않아서 여기서는 유효하지 않습니다
let s = "hello"; // 이 지점부터 s가 유효합니다
// s로 어떤 작업을 합니다
}
{라는 스코프 안에서만 값이 유효함
스코프 밖으로 벗어나면 유효기간이 만료
문자열 리터럴은 컴파일 타임에 내용을 알 수 있으므로, 텍스트가 최종 실행파일에 하드코딩됩니다. 이 방식은 빠르고 효율적이지만, 문자열이 변하지 않을 경우에만 사용할 수 있습니다. 컴파일 타임에 크기를 알 수 없고 실행 중 크기가 변할 수도 있는 텍스트는 바이너리 파일에 집어넣을 수 없죠.
반면 String 타입은 힙에 메모리를 할당하는 방식을 사용하기 때문에 텍스트 내용 및 크기를 변경할 수 있습니다. 하지만 이는 다음을 의미하기도 합니다:
1. 실행 중 메모리 할당자로부터 메모리를 요청해야 합니다.
2. String 사용을 마쳤을 때 메모리를 해제할 (즉, 할당자에게 메모리를 반납할) 방법이 필요합니다.
첫 번째는 이미 우리 손으로 해결했습니다. String::from 호출 시, 필요한 만큼 메모리를 요청하도록 구현되어 있거든요. 프로그래밍 언어 사이에서 일반적으로 사용하는 방식으로 구현
두번째는 가비지 컬렉터 (garbage collector, GC) 를 갖는 언어에서는 GC가 사용하지 않는 메모리를 찾아 없애주므로 프로그래머가 신경 쓸 필요 없습니다. GC가 없는 대부분의 언어에서는 할당받은 메모리가 필요 없어지는 지점을 프로그래머가 직접 찾아 메모리 해제 코드를 작성해야 합니다.
러스트에서는 이 문제를 변수가 자신이 소속된 스코프를 벗어나는 순간 자동으로 메모리를 해제하는 방식으로 해결
러스트는 변수가 스코프 밖으로 벗어나면 drop이라는 특별한 함수를 호출
let x = 5;
let y = x;
이건 그냥 값 자체를 STACK에 저장하는 구조
let s1 = String::from("hello");
let s2 = s1;
얕은 복사( String은 Heap 영역)
이렇게 깊은 복사가 되어야 함.
왜냐하면 S1, S2가 drop 될때(스코프에서 벗어날 때) 두번 drop되기 떄문임.
메모리 안정성을 보장하기 위해서, 러스트는 let s2 = s1; 라인 뒤로는 s1이 더 이상 유효하지 않다고 판단
이후 S1를 사용한다면, 에러
위는 하나의 값이 유효하지 않게 됨, 얕은 복사를 하면
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
위와 같이 깊은 복사를 해야함
fn main() {
let s = String::from("hello"); // s가 스코프 안으로 들어옵니다
takes_ownership(s); // s의 값이 함수로 이동됩니다...
// ... 따라서 여기서는 더 이상 유효하지 않습니다
let x = 5; // x가 스코프 안으로 들어옵니다
makes_copy(x); // x가 함수로 이동될 것입니다만,
// i32는 Copy이므로 앞으로 계속 x를
// 사용해도 좋습니다
} // 여기서 x가 스코프 밖으로 벗어나고 s도 그렇게 됩니다. 그러나 s의 값이 이동되었으므로
// 별다른 일이 발생하지 않습니다.
fn takes_ownership(some_string: String) { // some_string이 스코프 안으로 들어옵니다
println!("{}", some_string);
} // 여기서 some_string이 스코프 밖으로 벗어나고 `drop`이 호출됩니다.
// 메모리가 해제됩니다.
fn makes_copy(some_integer: i32) { // some_integer가 스코프 안으로 들어옵니다
println!("{}", some_integer);
} // 여기서 some_integer가 스코프 밖으로 벗어납니다. 별다른 일이 발생하지 않습니다.
S는 Heap 영역이므로 소유권이 takes_ownership으로 이동.
x는 Stack에 저장된 값 이므로, 소유권이 이동되는 것이 아니라, 그저 값이 복사됨
어떤 값을 다른 변수에 대입하면 값이 이동하고, 힙에 데이터를 갖는 변수가 스코프를 벗어나면, 사전에 해당 데이터가 이동하여 소유권이 다른 변수에 이동되지 않은 이상 drop 에 의해 데이터가 제거
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len()은 String의 길이를 반환합니다
(s, length)
}
소유권을 돌려받기 위해서는 다음과 같이 해야함.
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
let mut s = String::from("hello");
let r1 = &s; // 문제없음
let r2 = &s; // 문제없음
let r3 = &mut s; // 큰 문제
println!("{}, {}, and {}", r1, r2, r3);
오류 -> 가변 참조 두번은 안됨
어떤 값에 대한 불변 참조자가 있는 동안 같은 값의 가변 참조자를 만드는 것 또한 불가능
let mut s = String::from("hello");
let r1 = &s; // 문제없음
let r2 = &s; // 문제없음
println!("{} and {}", r1, r2);
// 이 지점 이후로 변수 r1과 r2는 사용되지 않습니다
let r3 = &mut s; // 문제없음
println!("{}", r3);
컴파일러가 더이상 사용되지 않는 지점을 알 수 있기 떄문에, 위의 경우에는 문제 없음
댕글링 포인터 (dangling pointer) 란, 어떤 메모리를 가리키는 포인터가 남아있는 상황에서 일부 메모리를 해제해 버림으로써, 다른 개체가 할당받았을지도 모르는 메모리를 참조하게 된 포인터를 말합니다. 포인터가 있는 언어에서는 자칫 잘못하면 이 댕글링 포인터를 만들기 쉽다.
fn dangle() -> &String { // dangle은 String의 참조자를 반환합니다
let s = String::from("hello"); // s는 새로운 String입니다
&s // String s의 참조자를 반환합니다
} // 여기서 s는 스코프 밖으로 벗어나고 버려집니다. 해당 메모리는 해제됩니다.
// 위험합니다!
그냥 s 반환은 가능
참조자만 남았지만, 소유권은 없는 이미 삭제된 포인터.
슬라이스 (slice) 는 컬렉션 (collection) 을 통째로 참조하는 것이 아닌, 컬렉션의 연속된 일련의 요소를 참조하도록 해줍니다. 슬라이스는 참조자의 일종으로서 소유권을 갖지 않음
let s = String::from("hello world");
let hello = s[0..5]; // 오류 발생
let world = &s[6..11]; // success, 참조자 타입만
구조체
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
. 으로 필드명에 대한 값을 호출 가능
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
이때, 라이프타임이 명시돼야 한다며 컴파일러가 에러를 일으킴
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
#[derive(Debug)]
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()
);
}
implement로 Ractangle참조를 인수로 받는 method를 다음과 같이 반환
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
$self 외의도, 매개변수로 가질 수 있음.
연관 함수 (Associated Function)
- 연관 함수는 특정 타입(구조체, 열거형 등)과 관련된 함수입니다.
- impl 블록 내에서 정의되며, 해당 타입의 인스턴스를 사용하지 않는 함수입니다. 즉, self를 첫 번째 매개변수로 받지 않습니다.
- 연관 함수는 메서드가 아니며 동작을 위해 인스턴스가 필요하지 않습니다.
주요 특징:
- 구조체 인스턴스가 필요 없는 함수:
- self를 첫 매개변수로 받지 않기 때문에 인스턴스를 생성하지 않고 호출할 수 있습니다.
- 예: String::from 함수처럼 인스턴스를 만들지 않고 사용할 수 있는 함수.
- 생성자로 자주 사용:
- 연관 함수는 구조체의 새 인스턴스를 반환하는 생성자로 자주 사용됩니다.
- 예를 들어, Rectangle::square(3)처럼, 정사각형을 만들 때 하나의 매개변수만 받아서 너비와 높이를 동일하게 설정하는 기능을 제공할 수 있습니다.
- 함수 호출:
- 구조체 이름 뒤에 ::를 붙여 호출합니다. 예: Rectangle::square(3).
코드 예시:
impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } }
// Rectangle::square(3) 을통해 연관함수 호출
열거형
enum IpAddrKind { V4, V6, }
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// 인스턴스 생성
fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
예시
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
- Quit은 연관된 데이터가 전혀 없습니다.
- Move은 구조체처럼 이름이 있는 필드를 갖습니다.
- Write은 하나의 String을 가집니다.
- ChangeColor는 세 개의 i32을 가집니다.
러스트에는 널이 없음, 값의 존재 혹은 부재의 개념을 표현할 수 있는 열거형 존재
-> OPTION(표준라이브러리) 임포트하는 목록인 프렐루드에도 포함
따라서 Some, None 배리언트 앞에 Option::도 붙이지 않아도 됨
enum Option<T> {
None,
Some(T),
}
fn find_item(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Item 1")) // 값이 있을 경우
} else {
None // 값이 없을 경우
}
}
fn main() {
let result = find_item(1);
match result {
Some(item) => println!("Found: {}", item), // Some을 만나면 실행
None => println!("Item not found"), // None을 만나면 실행
}
}
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
match 같이 None 에 대한 경우의 수를 안 따지면, compile error
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
// 위와 아래는 동일한 방식
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
'기타' 카테고리의 다른 글
| 수명 (0) | 2025.03.16 |
|---|---|
| 백업용 링크 (2) | 2024.09.07 |
| BoB 12기 보안제품개발 트랙 회고 (1) | 2024.03.24 |