RUST
플러터 desktop 앱을 만들기 위해서는 C++로 DLL을 컴파일해서 쓰고는 있는데, 비주얼 스튜디오가 켜기도 귀찮고, 쓰기도 귀찮다. 그러다 rust를 봤는데, 언어는 모르겠지만, 설치가 간편하다. C++을 넘어서긴 어렵겠지만, 그래도 내 수준에서는 그럭저럭 사용할 수 있지 않을까 싶다. 대충. 내용을 정리해본다.
설치
온갖 프로그램 랭귀지 중에 이렇게 설치가 편한 프로그램이 있었던가.. 싶다. 물론 C++의 라이브러리를 사용하기 때문일지는 모르겠지만, 일단 아래 사이트에서 프로그램을 다운받아 엔터 한번 치는 것으로 모든 설치는 끝나는 듯하다.
Install Rust
A language empowering everyone to build reliable and efficient software.
rust-lang.org
powershell에서 아래 명령어를 입력하여 제대로 설치가 되었는지 확인할 수 있다.
프로젝트를 생성, 빌드도 명령어 한줄로 가능하다. (아.. VS에서 빌드 버튼을 찾기까지 얼마나 우여곡절을 겪었는지 모른다..)
쌩기초
변수와 불변성 (let, mut)
아무리 chatGPT를 사용한다고 하더라도, 변수 선언 정도는 알고 있어야겠지. 그런데.. 음... 첫 느낌부터 뭔가 다르긴 하네..
// 0201
fn main (){
// 변수 선언 (let)
let x = 5 ;
println ! ( "x = { } " , x );
// x = 6; // 컴파일 에러 : 불변 변수에 값 재할당 불가
// 가변 변수 (let mut)
let mut y = 10 ;
println ! ( "y = { } " , y );
y = 20 ; // 가능
println ! ( "y = { } " , y );
// 상수 (const)
const MAX_POINTS : u32 = 100_000 ; // 대문자? , 반드시 타입 기재
println ! ( "Max = { } " , MAX_POINTS );
//Shadowing (변수 가리기)
// 같은 이름으로 다시 선언 가능, 이전 값은 사라지고, 새로운 값으로 덮어쓰기, 타입까지 바꿀 수 있음
let z = 5 ;
let z = z + 1 ;
let z = z * 2 ;
println ! ( "z = { } " , z );
let spaces = " " ;
let spaces = spaces . len (); // 문자열 -> 숫자로 변환
println ! ( "spaces = { } " , spaces );
}
데이터 타입 (스칼라, 복합 타입: 튜플, 배열, 슬라이스)
언제나 그렇듯 데이터 타입은 봐도 모르겠고, 뭐가 도움이 되는지도 잘 모르겠다.
// 0202
fn main (){
// Scalar type
// integer
// 부호 있는 정수 i8, i16, i32, i64, i128, isize
// 부호 없는 정수 u8, u16, u32, u64, u128, usize
// 기본 정수 타입 : i32
// usize, isize는 플랫폼 크기(32/64) 비트에 따라 달라짐
let a : i32 = 42 ;
let b : i64 = 100 ;
println ! ( "a = { } , b = { } " , a , b );
// 부동 소수점 (floating - point)
// f32, f64
let pi = 3.1415 ;
let e : f32 = 2.718 ;
println ! ( "pi = { } , e = { } " , pi , e );
// boolean
let is_rust_fun : bool = true ;
println ! ( " { } " , is_rust_fun );
// 문자(char)
// char은 유니코드 스칼라 값(64)
// 한글, 이모지 모두 가능
let c = 'A' ;
println ! ( " { } " , c );
// 복합 타입
// tuple
// 서로 다른 타입의 값을 묶을 수 있음
let tup = ( 500 , 6.4 , 'z' );
let ( x , y , z ) = tup ;
println ! ( "y = { } " , y );
println ! ( "z = { } " , tup . 2 );
// array
// 같은 타입의 값이 고정된 길이로 저장됨
// 길이가 컴파일 타임에 고정됨
let arr = [ 1 , 2 , 3 , 4 , 5 ];
let first = arr [ 0 ];
let len = arr . len ();
println ! ( "first = { } , length = { } " , first , len );
// slice
// 배열이나 문자열의 일부분을 참조
// 길이는 런타임에 결정 가능
let arr2 = [ 10 , 20 , 30 , 40 , 50 ];
let slice = & arr2 [ 1 .. 4 ];
println ! ( "slice = { : ? } " , slice );
}
함수 정의와 호출
이건.. 비슷하겠지..
// 0203
fn main (){
say_hello ();
let sum = add ( 5 , 7 );
println ! ( "sum = { } " , sum );
println ! ( " { } " , sign ( - 3 ));
println ! ( " { } " , sign ( 0 ));
println ! ( " { } " , sign ( 5 ));
// rust에서는 표현식과 문이 엄격히 구분됨
// 표현식 : 값이 생김.
// 문 : 값이 없음. (void)
let number = {
let x = 3 ;
x + 1 //표현식- 4 반환.
};
println ! ( "number = { } " , number );
}
fn say_hello () {
println ! ( "Hello, Rust" );
greet ( "Alice" , 3 );
}
// parameter
fn greet ( name : & str , count : i32 ) {
for _ in 0 .. count {
println ! ( "Hello, { } " , name )
}
}
// return
fn add ( a : i32 , b : i32 ) -> i32 { //반환타입
a + b //세미콜론 없음 -> 반환 , 세미콜론이 있으면 단순한 문, 없으면 표현식.
}
// keyword return
// 중간에 값을 반환받을 때.
fn sign ( x : i32 ) -> & ' static str {
if x < 0 {
return "negative" ;
}
if x == 0 {
return "zero" ;
}
"positive"
}
제어 흐름 (if, match, loop, while, for)
쓸 일이 있으려나...
// 0204
fn main (){
// if
let number = 7 ;
if number < 10 {
println ! ( "small number" );
} else {
println ! ( "big number" );
}
let result = if number % 2 == 0 { "even" } else { "odd" };
println ! ( "result = { } " , result );
//match
let x = 3 ;
match x {
1 => println ! ( "one" ),
2 | 3 => println ! ( "two or three" ), // 여러값 매칭
4 ..= 10 => println ! ( "between 4 and 10" ), // 범위 매칭
_ => println ! ( "something else" ), //_ wild card , 모든값 매칭. //모든 경우를 반드시 처리해야함
}
//자주쓰는 타입과 match
match divide ( 10 , 2 ) {
Some ( val ) => println ! ( "Result = { } " , val ),
None => println ! ( "Cannot divide by zero" ),
}
// loop
let mut count = 0 ;
loop {
count += 1 ;
if count == 3 {
continue ; // 3일때는 건너뛰기.
}
if count > 5 {
break ; //루프 종료.
}
println ! ( "count = { } " , count );
}
// loop - break의 값 반환
let mut n = 0 ;
let result = loop {
n += 1 ;
if n == 5 {
break n * 2 ; // break - return
}
};
println ! ( "result = { } " , result );
//while 조건이 참인 동안 루프 실행
let mut n = 3 ;
while n > 0 {
println ! ( " { } !" , n );
n -= 1 ;
}
println ! ( "Liftoff!" );
// for : 컬렉션, 배열, 범위를 반복할때 사용
let arr = [ 10 , 20 , 30 ];
for val in arr . iter () {
println ! ( " { } " , val );
}
for i in 1 ..= 3 { // 끝을 포함
println ! ( "i = { } " , i );
}
}
fn divide ( a : i32 , b : i32 ) -> Option < i32 >{
if b == 0 {
None
} else {
Some ( a / b )
}
}
소유권과 빌림
소유권(Ownership) 규칙
// 0301
fn main (){
// 소유권 시스템
// 1. rust의 각 값은 하나의 소유자만 가진다.
// 2. 소유자가 스코프를 벗어나면 해당값은 자동으로 해제 된다.
// 3. 어떤 값의 소유권을 다른 변수에 넘기면, 기존 변수는 더 이상 사용할 수 없다.
let s1 = String :: from ( "hello" );
let s2 = s1 ; // 소유권 이동(move)
// println!("{}", s1); // 에러 s1은 더 이상 유효하지 않음
println ! ( " { } " , s2 ); // 가능 s2가 소유권을 가짐
// 깊은 복사 (clone) 와 얕은 이동 (move)
// Move : 메모리 복사를 하지 않고 단순히 소유권만 이전 - 성능 최적화
// Clone : 실제 힙 데이터를 깊게 복사 - 원본과 복제본 모두 사용 가능
let s3 = String :: from ( "hello" );
let s4 = s3 . clone ();
println ! ( "s3 = { } " , s3 );
println ! ( "s4 = { } " , s4 ); // 둘 다 사용 가능
// 스택(stack)과 힙(heap)에 따른 소유권 차이
// 정수, 불리언 같은 스택값은 copy 트레잇을 가지고 있어 이동 대신 복사가 일어남
// string , vec 같은 힙 값은 소유권 이동이 발생함
let x = 5 ;
let y = 'x' ; //스택값은 copy됨
println ! ( "x = { } , y = { } " , x , y );
// rust의 소유권 시스템은 메모리를 안전하게 관리하고, 동시에 불필요한 런타임 비용을 없애기 위한
// 핵심 개념임. 이 원리를 이해하면 이후의 빌림과 라이프타임을 자연스럽게 이해가능.
}
빌림(Borrowing)과 참조
// 0302
// borrowing
// Rust는 소유권을 엄격하게 관리하기 때문에, 변수를 함수에 넘기면 소유권이 이동해버립니다. (move)
// 매번 소유권을 주고 받으면 불편하기 때문에, 참조... 즉 빌려쓰기 개념이 필요
fn main (){
// 불변참조(&T)
// 소유권을 넘기지 않고 값을 읽을 수 있음
// 동시에 여러개 존재할 수 있음
let s = String :: from ( "hello" );
print_length ( & s ); // 소유권 이동 없음
println ! ( "s는 여전히 사용 가능: { } " , s );
// 가변참조 (&mut T)
// 값을 수정하려면 가변 참조를 사용해야함
// 단 동시에 오직 하나만 존재 할 수 있음
// &mut t를 빌려주면, 그 동안은 t에 직접 접근할 수 없음
// 이 규칙 덕분에 데이터 경쟁(race condition)을 컴파일 타임에 차단할 수 있음.
let mut t = String :: from ( "hello2" );
change ( & mut t );
println ! ( " { } " , t );
// 불변 참조와 가변 참조의 규칙
// 규칙1. 불변참조는 여러개 가능.
// 규칙2. 가변참조는 한번에 하나만 가능.
// 규칙3. 불변참조와 가변참조는 동시에 가질 수 없음
let mut x = String :: from ( "hello" );
let r1 = & x ; //불변참조 1
let r2 = & x ; //불변참조 2
println ! ( " { } { } " , r1 , r2 );
let r3 = & mut x ; // 가변참조
println ! ( " { } " , r3 );
// println!("{} {}", r1,r2); 사용이 불가능하다.
}
fn print_length ( s :& String ) {
println ! ( "length : { } " , s )
}
fn change ( t :& mut String ) {
t . push_str ( ", world!" );
}
// Dangling Reference
// 해제된 메모리를 참조하는 문제가 발생하지 않음
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // s는 함수가 끝나면 drop
// }
가변 참조 vs 불변 참조
// 0303
fn main (){
// 불변참조(&)
let s = String :: from ( "hello" ) ;
let r1 = & s ;
let r2 = & s ;
println ! ( " { } and { } " , r1 , r2 );
// 가변참조(&mut)
// 단 하나만 존재 가능, 데이터 변경 가능, 동시에 여러 가변참조가 있으면 데이터 경합 발생 - 금지
let mut s2 = String :: from ( "hello" );
let r3 = & mut s2 ;
r3 . push_str ( ", Rust!" );
println ! ( " { } " , r3 );
// 불변참조와 가변참조의 충돌 ; 혼용 불가
let mut x = String :: from ( "Rust" );
let x1 = & x ;
let x2 = & x ;
// let x3 = &mut x; // 불변참조가 끝나기 전에 가변참조 생성 - 컴파일 에러
// println!("{}, {}, {}", x1, x2, x3);
// 참조의 스코프 규칙
println ! ( " { } , { } " , x1 , x2 ); // 불변참조 사용 완료
let x3 = & mut x ; // 불변참조가 끝나고 난 이후 참조
println ! ( " { } " , x3 );
}
수명(Lifetime) 기초
// 0304
//Lifetime이 필요한 이유
// Rust는 메모리를 자동으로 해제합니다. (GC가 없고, 소유권 규칙으로 관리)
// 문제는 **참조(Reference)**가 등장할 때입니다.
fn main () {
// 예를 들어, 아래 코드는 dangling reference(죽은 참조) 문제를 일으킵니다:
// 이런 상황을 막기 위해 Rust 컴파일러는 참조의 유효 범위를 추적해야 합니다. 이때 등장하는 개념이 **수명(Lifetime)**입니다.
// let r; // 참조 변수 r 선언
// {
// let x = 5;
// r = &x; // x에 대한 참조
// } // x는 여기서 소멸
// println!("r: {}", r); //r은 소멸된 x를 가르키고 있음
// Lifetime 기본 개념
// 수명은 **참조가 유효한 범위(scope)**를 나타냅니다.
// 컴파일러는 모든 참조가 올바른 수명 내에서만 사용되는지 검사합니다.
// 대부분의 경우 Rust가 추론해주므로 직접 쓰지 않아도 됩니다.
// 하지만 함수, 구조체 등에서 여러 참조가 얽힐 때는 명시적 Lifetime 표기가 필요합니다.
// Lifetime 표기법
// Lifetime은 'a, 'b 같은 방식으로 작성합니다.
// 예: 함수에서 두 참조를 받아 더 긴 쪽을 반환하기
let s1 = String :: from ( "Hello" );
let s2 = "World!" ;
let result = longest ( s1 . as_str (), s2 );
println ! ( "The longest string is { } " , result );
// 구조체에서의 Lifetime
// 구조체 안에 참조를 저장하려면 Lifetime이 필요합니다.
// 여기서 'a는 Book 인스턴스가 살아 있는 동안 title 참조도 유효해야 함을 보장합니다.
let name = String :: from ( "Rust in Action" );
let b = Book { title : & name };
println ! ( "Book title: { } " , b . title );
// Lifetime Elision (생략 규칙)
// Rust는 자주 쓰이는 패턴에서는 Lifetime을 자동으로 추론해줍니다.
let answer = first_word ( & name );
println ! ( "First word: { } " , answer );
}
// 여기서 'a는 입력 참조(s1, s2)와 반환 참조가 모두 같은 수명을 공유해야 한다는 규칙을 나타냅니다.
// 즉, 반환값은 입력 참조 중 더 오래 사는 쪽에 종속됩니다.
fn longest < ' a >( s1 : & ' a str , s2 : & ' a str ) -> & ' a str {
if s1 . len () > s2 . len () { s1 } else { s2 }
}
struct Book < ' a > {
title : & ' a str ,
}
fn first_word ( s : & str ) -> & str { //'a 생략됨
& s [ .. 1 ]
}
어느정도 이해가 됐다면, 궁금증이 많을텐데, 전혀 이해가 가지 않는 관계로 궁금증도 안생긴다. 좀 쉬었다 가기로 하자.