안녕하세요, 단테입니다.
지난 1달 동안 비욘드 JS 러스트 시리즈를 통해 러스트의 기본 문법과 개념에 대해 공부해봤습니다.
이제부터 러스트를 실제 프로젝트에 사용할 수 있을 정도로 사용할 수 있게 익숙해지는 과정을 겪겠습니다.
같이 화이팅 해볼까요?
변수, 함수, struct, trait등 각 객체마다 선호하는 네이밍 컨벤션이 다릅니다.
fn read_str // O
fn readStr // X
struct HttpClient {}
러스트의 문법은 여러가지 언어들의 문법을 혼합해 놓은 것들, 그리고 러스트 만의 고유 문법(lifetime names, patterns, macros, attributes) 두 가지로 이뤄져 있습니다.
변수 선언은 타입스크립트, 코틀린과 유사하나 자바, C와는 차이점이 있습니다.
// typescript
const s: string = "";
let n: number = 0.9;
let i = 123 // Type inferred
// rust
let s: &str = "";
let mut n: f64 = 0.9;
let mut i = 123; // Type inferred
대부분의 변수 선언은 타입추론을 통해 타입을 명시적으로 지정해주지 않아도 컴파일러가 올바른 변수 타입을 이해할 수 있습니다.
다음 타입은 primitive data types
입니다.
bool : The boolean type.
char : A character type.
i8 : The 8-bit signed integer type.
i16 : The 16-bit signed integer type.
i32 : The 32-bit signed integer type.
i64 : The 64-bit signed integer type.
isize : The pointer-sized signed integer type.
u8 : The 8-bit unsigned integer type.
u16 : The 16-bit unsigned integer type.
u32 : The 32-bit unsigned integer type.
u64 : The 64-bit unsigned integer type.
usize : The pointer-sized unsigned integer type.
f32 : The 32-bit floating point type.
f64 : The 64-bit floating point type.
array : A fixed-size array, denoted [T; N], for the element type, T, and the non-negative compile-time constant size, N.
slice : A dynamically-sized view into a contiguous sequence, [T].
str : String slices.
tuple : A finite heterogeneous sequence, (T, U, ..).
void type은 unit
이라고 부르며 ()와 같이 표현 되어집니다.
//java
int i = 123;
long l = 456L;
float f = 0.5;
double d = 0.5;
String string = "Herllo";
int [] arr = {1,2,3};
List<Integer> list = Arrays.asList(1,2,3);
// rust
let i: i32 = 123;
let l: i64 = 456;
let f: f64 = 0.5
let d: f64 = 0.5f64;
let string: &str = "Hello";
let arr: [i32; 3] = [1,2,3];
let list: vec<i32> = vec![1,2,3]
러스트에서는 숫자형 타입에는 suffix에 타입을 붙일 수 있습니다.
1000u32 or 0.5f64
자바스크립트의 const let
, 코틀린의 val var
과 유사하게 러스트에서는 let
let mut
를 구분해서 사용해야 합니다. mut로 선언되지 않으면 기존 값을 변경하지 못합니다.
// typescript
let arr1: string[] = [];
arr1.push("123"); // OK
arr1 = ["a", "b"]; // OK
const arr2: string[] = [];
arr2.push("123") // OK, even though arr2 is const
arr2 = []; // error, arr2 is const
let mut arr1 = vec![];
arr1.push("123"); // OK
arr1 = vec!["a", "b"]; // OK
let arr2 = vec![];
arr2.push("123") // error, arr2 is not mutable;
arr2 = vec![]; // error, arr2 is not mutable;
러스트에서도 자바스크립트와 코틀린처럼 구조분해를 사용할 수 있습니다.
// js
function distance(a,b) {
const {x: x1, y: y1} = a;
const {x: x2, y: y2} = b;
return Math.sqrt(
Math.pow(x2 - x1, y2) + Math.pow(y2 - y1, 2)
)
// rust
fn distance(a: &Point, b: &Point) -> f32 {
let Point {x: x1, y: y1} = a;
let Point {x: x2, y: y2} = b;
((x2 - x1).powf(2.0) + (y2 -y1).powf(2.0)).sqrt()
}
struct Point {
x: f32,
y: f32,
}
// c
void log(char * message) {
printf("INFO &S\n", message);
}
// rust
fn log(message: &str) -> () {
println!("INfO {}", message);
}
unit type ()
은 다른 언어의 void와 동일합니다. 예제 코드에서는 fn log(message: &str) { ... }
와 같이 ㅅ애략할 수 있습니다.
다른 언어와 마찬가지로 러스트에서도 inner function을 선언할 수 있습니다.
// js
const RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
function isValidRange(start, end) {
function isValid(date) {
return date && date.match(RE);
}
return isValid(start) && isValid(end);
}
use regex:Regex;
const RE: &str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$";
fn is_valid_range(start: &str, end: &str) -> bool {
fn is_valid(date: &str) -> bool {
!date.is_empty()
&& Regex::new(RE)
.unwrap()
.is_match(date)
}
is_valid(start) && is_valid(end)
}
코틀린와 같이 extension methods를 사용할 수 있습니다.
// kotlin
typealias Range = Pair<String, String>
fun Range.isValid(): Boolean {
val (start, end) = this
return start.isNotEmpty() && end.isNotEmpty()
}
object Main {
@JvmStatic
fun main(args: Array<String>) {
val range = Range("2020-01-01", "2020-12-31")
if (range.isValid()) {
println("Range is valid!")
}
}
}
type Range<'r> = (&'r str, &'r str);
trait IsValid {
fn is_valid(&self) -> bool;
}
impl<'r> IsValid for Range<'r> {
fn is_valid(&self) -> bool {
let (start, end) = &self;
!start.is_empty() && !end.is_empty()
}
}
fn main() {
let range = ("2020-01-01", "2020-12-31");
if range.is_valid() {
println!("Range is valid!");
}
}
러스트는 다른 언어의 람다,화살표 함수, 무명 함수와 같은 개념의 클로져를 지원합니다.
클로져 외부에서 변수를 읽을 때 러스트는 자바스크립트나 자바보다 더욱 엄격한 규칙을 적용합니다.
// js
function findEmails(list) {
return list.filter( s => s && s.includes("@") );
}
// rust
fn find_emails(list: Vec<String>) -> Vec<String> {
list.into_iter()
.filter(|s| s.contains("@"))
.collect()
}
러스트에서는 코틀린과 같이 웬만한 모든 것들이 expression들로 선언할 수 있습니다. 자바스크립트와 자바는 그렇지 않습니다. if expression을 이용해 패턴 바인딩을 할 수 있습니다.
// js
function getLogLevel() {
let level = process.env.TRACE
? "trace"
: process.env.DEBUG
? "debug"
: "info";
level =
level === "trace"
? 0
: level === "debug"
? 1
: 2;
console.log("using log level", level);
return level;
}
// rust
fn get_log_level() -> u32 {
let level = if std::env::var("TRACE").is_ok() {
"trace"
} else if std::env::var("DEBUG").is_ok() {
"debug"
} else {
"info"
};
let level = match level {
"trace" => 0,
"debug" => 1,
_ => 2,
};
println!("using log level {}", level);
level
}
러스트는 자바나 타입스크립트처럼 클래스를 지원하지 않고 structs를 사용하게 합니다. 구조체는 클래스와 같이 데이터와 메소드를 가질 수 있는 컨테이너이나 객체지향에서 제공하는 상속을 사용할 수 없습니다.
// java
public class HttpClient {
private final ClientImpl clientImpl;
public HttpClient() {
clientImpl = new ClientImpl();
}
public String get(String url) {
return clientImpl.newRequest()
.get(url).asString();
}
}
public static void main(String[] args) {
HttpClient httpClient = new HttpClient();
System.out.pringln(httpClient.get("https://example.com"));
}
// rust
pub struct HttpClient {
client_impl: ClientImpl,
}
impl HttpClient {
pub fn new() -> HttpClient {
HttpClient {
client_impl: ClientImpl {}
}
}
pub fn get(&self, url: &str) -> String {
self.client_impl.new_request()
.get(url)
.as_string()
}
}
fn main() {
let http_client = HttpClient::new();
println!("{}", http_client.get("https://example.com/"));
}
구조체는 자바나 자바스크립트처럼 this를 암시적으로 사용할 수는 없습니다. 따라서 명시적으로 argument에 self를 선언하여 참조해야 합니다. 이러한 문법은 파이썬과 유사하죠.
이 파라메터가 없는 메소드는 associated function이라고 합니다. 이 associated function은 다른 언어에서는 static method라고 합니다. 비욘드 js 러스트 강의에서 associated function을 설명할 때 이야기 했지만 대부분 struct의 새로운 인스턴스를 반환할때 사용하며 이때 주로 사용하는 new
메소드 명은 특별한 의미를 가지지는 않습니다.
러스트에는 cosntructor가 없습니다. 구조체는 자바스크립트의 객체와 비슷하게 만들어집니다. 구조체에 생성자가 필요하다면 보통 associated function을 사용하는 것이 관례이고 이 때 new라는 함수명을 사용합니다.
러스트의 특성은(trait) 다른 언어의 interface와 비슷한 역할을 합니다.
// kotlin
interface Named {
fun name(): String
}
data class User(
val id: Int,
val name: String
) : Named {
override fun name(): String {
return this.name
}
}
// rust
pub trait Named {
fn name(&self) -> &String;
}
pub struct User {
pub id: i32,
pub name: String,
}
impl Named for User {
fn name(&self) -> &String {
return &self.name;
}
}
러스트는 정적 언어이기 때문에 다른 언어에서 런타임에 실행하는 것을 가능한한 컴파일 타임에 대신 실행합니다. 인터페이스는 dynamic dispatch를 사용하기 원할 때 주로 사용합니다.
trait은 자바나 코틀린의 인터페이스와 같이 default method를 지원합니다.
default method는 특성 구현에서 오버라이딩 될 수 있습니다.
trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
struct NewsArticle {
headline: String,
location: String,
author: String,
content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
자바나 코틀린처럼 열거형을 지원하지만 좀 더 유연함을 제공합니다. 러스트의 열거형은 C++, 타입스크립트의 열거형 보다 좀 더 많은 기능을 제공합니다.
// java
enum UserRole {
RO("read-only"), USER("user"),
ADMIN("administrator");
private final String name;
UserRole(String name) {
this.name = name;
}
String getName() {
return name;
}
boolean isAccessAllowed(String httpMethod) {
switch (httpMethod) {
case "HEAD":
case "GET":
return true;
case "POST":
case "PUT":
return this == USER || this == ADMIN;
case "DELETE":
return this == ADMIN;
default:
return false;
}
}
}
class Main {
public static void main(String[] args) {
UserRole role = UserRole.RO;
if (role.isAccessAllowed("POST")) {
System.out.println("OK: "
+ role.getName());
} else {
System.out.println("Access denied: "
+ role.getName());
}
}
}
// rust
#[derive(PartialEq)]
enum UserRole {
RO,
USER,
ADMIN,
}
impl UserRole {
fn name(&self) -> &str {
match *self {
UserRole::RO => "read-only",
UserRole::USER => "user",
UserRole::ADMIN => "administrator",
}
}
fn is_access_allowed(
&self,
http_method: &str,
) -> bool {
match http_method {
"HEAD" | "GET" => true,
"POST" | "PUT" => {
*self == UserRole::USER
|| *self == UserRole::ADMIN
}
"DELETE" => *self == UserRole::ADMIN,
_ => false,
}
}
}
fn main() {
let role = UserRole::RO;
if role.is_access_allowed("POST") {
println!("OK: {}", role.name());
} else {
println!("Access denied: {}", role.name());
}
}
러스트에서는 const enum을 지원하지 않습니다.
예를 들어 다음은 되지만
enum MyEnum {
A(i32),
B(i32),
}
다음은 안됩니다.
enum MyEnum {
A(i32),
B(i32),
}
따라서 이를 우회하는 방법으로 패턴 매칭을 사용해볼 수 있습니다.
이 enum match들은 컴파일 타임에 체크될 수 있습니다. 모든 열거형 variant가 사용되면
enum MyEnum {
A,
B,
}
impl MyEnum {
fn value(&self) -> i32 {
match *self {
MyEnum::A => 123,
MyEnum::B => 456,
}
}
}
러스트의 열거형은 associated values를 지원합니다. constant가 아니고 특정 값에 따라 variant instance를 만들 수 있습니다.
// kotlin
sealed class GitCommand {
abstract fun execute()
}
object Status : GitCommand() {
override fun execute() =
executeCommand(listOf("status"))
}
class Checkout(
private val branch: String
) : GitCommand() {
override fun execute() =
executeCommand(listOf("checkout", branch))
}
class Add(
private val files: List<String>
) : GitCommand() {
override fun execute() =
executeCommand(listOf("add") + files)
}
class Log(
private val decorate: Boolean,
private val patch: Boolean
) : GitCommand() {
override fun execute() {
val args = mutableListOf("log")
if (decorate) {
args.add("--decorate")
}
if (patch) {
args.add("--patch")
}
executeCommand(args)
}
}
fun executeCommand(args: List<String>) {
val redirect = ProcessBuilder.Redirect.INHERIT
ProcessBuilder(listOf("git") + args)
.redirectInput(redirect)
.redirectOutput(redirect)
.redirectError(redirect)
.start()
.waitFor()
}
object Main {
@JvmStatic
fun main(args: Array<String>) {
val command =
Log(decorate = false, patch = true)
command.execute()
}
}
// rust
use std::process::Command;
pub enum GitCommand<'g> {
STATUS,
CHECKOUT(&'g str),
ADD(Vec<&'g str>),
LOG { decorate: bool, patch: bool },
}
impl<'g> GitCommand<'g> {
fn execute(self) {
let args: Vec<&str> = match self {
GitCommand::STATUS => vec!["status"],
GitCommand::CHECKOUT(branch) => {
vec!["checkout", branch]
}
GitCommand::ADD(files) => {
[vec!["add"], files].concat()
},
GitCommand::LOG {
decorate,
patch,
} => {
let mut args = vec!["log"];
if decorate {
args.push("--decorate")
}
if patch {
args.push("--patch")
}
args
}
};
execute_command("git", &args);
}
}
fn execute_command(command: &str, args: &[&str]) {
Command::new(command)
.args(args)
.spawn()
.expect("spawn failed!")
.wait()
.expect("command failed!");
}
fn main() {
let command = GitCommand::LOG {
decorate: false,
patch: true,
};
command.execute();
}
코틀린의 sealed class는 rust의 enum과 유사합니다.
오늘은 러스트의 문법을 코틀린과 자바, 자바스크립트, C의 문법과 비교해봤습니다.
감사합니다.!
본 포스팅은 https://overexact.com/rust-for-professionals/ 의 콘텐츠를 기반으로 작성되었습니다.