녹 특성은 Go 인터페이스와 어떻게 다릅니 까?


64

나는 Go에 비교적 익숙하며 많은 작은 프로그램을 작성했습니다. 물론 녹에 대해서는 잘 모르지만 계속 지켜봐야합니다.

최근에 http://yager.io/programming/go.html을 읽은 후 기사가 실제로 인터페이스가 많지 않을 때 Go가 부당하게 비판하는 것처럼 보이므로 제네릭이 처리되는 두 가지 방법을 개인적으로 조사 할 것이라고 생각했습니다. 우아하게 성취 할 수 없었습니다. 나는 Rust 's Traits가 얼마나 강력했는지에 대한 과대 광고를 계속 들었고 Go에 대한 사람들의 비판 만했습니다. Go에서 약간의 경험을 쌓았을 때, 그것이 실제로 무엇이고 그 차이점이 무엇인지 궁금했습니다. 내가 찾은 것은 특성과 인터페이스가 매우 비슷하다는 것입니다! 궁극적으로, 내가 뭔가 빠졌는지 확신 할 수 없으므로 여기에 유사점에 대한 빠른 교육 요약이 있으므로 누락 된 것을 말해 줄 수 있습니다!

이제 설명서 에서 Go Interfaces를 살펴 보겠습니다 .

Go의 인터페이스는 객체의 동작을 지정하는 방법을 제공합니다. 무언가 할 수있는 경우 여기에서 사용할 수 있습니다.

지금까지 가장 일반적인 인터페이스는 Stringer객체를 나타내는 문자열을 반환합니다.

type Stringer interface {
    String() string
}

따라서 String()정의 된 Stringer객체 는 객체입니다. func (s Stringer) print()거의 모든 객체 를 가져 와서 인쇄하는 형식 서명에 사용할 수 있습니다 .

우리는 또한 interface{}어떤 물건을 가져야 합니다. 그런 다음 리플렉션을 통해 런타임에 유형을 결정해야합니다.


이제 Rust Traits의 문서를 살펴 보겠습니다 .

가장 간단하게, 특성은 0 개 이상의 메소드 서명 세트입니다. 예를 들어, 단일 메소드 서명으로 콘솔에 인쇄 할 수있는 항목에 대해 Printable 특성을 선언 할 수 있습니다.

trait Printable {
    fn print(&self);
}

이것은 바로 Go 인터페이스와 매우 유사합니다. 내가 볼 수있는 유일한 차이점은 단순히 메소드를 정의하는 것이 아니라 특성의 '구현'을 정의한다는 것입니다. 그래서 우리는

impl Printable for int {
    fn print(&self) { println!("{}", *self) }
}

대신에

fn print(a: int) { ... }

보너스 질문 : 특성을 구현하지만 사용하지 않는 함수를 정의하면 Rust에서 어떻게됩니까 impl? 그냥 작동하지 않습니까?

Go의 인터페이스와 달리 Rust의 형식 시스템에는 형식 매개 변수가 interface{}있어 컴파일러와 런타임에서 실제로 형식을 알고있는 동안 적절한 제네릭과 작업을 수행 할 수 있습니다 . 예를 들어

trait Seq<T> {
    fn length(&self) -> uint;
}

는 모든 유형에서 작동하며 컴파일러 리플렉션을 사용하지 않고 컴파일 타임에 시퀀스 요소의 유형을 알고 있습니다.


이제 실제 질문 : 여기에 차이점이 있습니까? 그들은 정말 비슷한? 여기서 누락 된 더 근본적인 차이점이 없습니까? (사용상. 구현 세부 사항은 흥미롭지 만 궁극적으로 동일하게 작동하면 중요하지 않습니다.)

구문상의 차이점 외에도 실제 차이점은 다음과 같습니다.

  1. Go는 implTrat를 구현하기 위해 자동 메소드 디스패치 대 Rust require (?) 를 가지고 있습니다.
    • 우아함과 명시 적
  2. Rust는 리플렉션없이 적절한 제네릭을 허용하는 타입 파라미터를 가지고 있습니다.
    • Go는 실제로 아무런 반응이 없습니다. 이것은 훨씬 더 강력한 유일한 방법이며 궁극적으로 다른 유형 서명을 사용하여 메서드를 복사하여 붙여 넣는 대신 사용할 수 있습니다.

이것 만이 사소한 차이가 아닌가? 그렇다면 Go 's Interface / Type 시스템은 실제로 인식 된 것만 큼 약하지 않은 것으로 보입니다.

답변:


59

특성을 구현하지만 impl을 사용하지 않는 함수를 정의하면 Rust에서 어떻게됩니까? 그냥 작동하지 않습니까?

특성을 명시 적으로 구현해야합니다. Rust에게는 이름 / 서명과 일치하는 메소드를 갖는 것은 의미가 없습니다.

일반 통화 발송

이것 만이 사소한 차이가 아닌가? 그렇다면 Go 's Interface / Type 시스템은 실제로 인식 된 것만 큼 약하지 않은 것으로 보입니다.

정적 디스패치를 ​​제공하지 않으면 특정 경우 (예 : Iterator아래에서 언급 한 경우) 성능이 크게 저하 될 수 있습니다 . 나는 이것이 당신이 의미하는 것 같아요

Go는 실제로 아무런 반응이 없습니다. 이것은 훨씬 더 강력한 유일한 방법이며 궁극적으로 다른 유형 서명을 사용하여 메서드를 복사하여 붙여 넣는 대신 사용할 수 있습니다.

차이점을 깊이 이해할 가치가 있기 때문에 더 자세히 다룰 것입니다.

녹에서

Rust의 접근 방식을 통해 사용자는 정적 디스패치와 동적 디스패치 중에서 선택할 수 있습니다 . 예를 들어

trait Foo { fn bar(&self); }

impl Foo for int { fn bar(&self) {} }
impl Foo for String { fn bar(&self) {} }

fn call_bar<T: Foo>(value: T) { value.bar() }

fn main() {
    call_bar(1i);
    call_bar("foo".to_string());
}

call_bar위 의 두 호출은 각각에 대한 호출로 컴파일됩니다.

fn call_bar_int(value: int) { value.bar() }
fn call_bar_string(value: String) { value.bar() }

.bar()메소드 호출은 정적 함수 호출, 즉 메모리의 고정 함수 주소에 대한 것입니다. 컴파일러는 어떤 함수가 호출 되는지 정확히 알고 있기 때문에 인라인과 같은 최적화가 가능합니다 . (이것은 때때로 "모노 모르 파이 제이션 (monomorphisation)"이라고 불리는 C ++의 기능입니다.)

이동 중

Go는 "일반적인"함수에 대해서만 동적 디스패치를 ​​허용합니다. 즉, 메소드 주소가 값에서로드 된 후 호출되므로 정확한 함수는 런타임에만 알려집니다. 위의 예를 사용하여

type Foo interface { bar() }

func call_bar(value Foo) { value.bar() }

type X int;
type Y string;
func (X) bar() {}
func (Y) bar() {}

func main() {
    call_bar(X(1))
    call_bar(Y("foo"))
}

이제이 두 가지는 call_bar항상 인터페이스의 vtable 에서로드 된 call_bar주소와 함께 위를 호출합니다 .bar

저수준

위의 문구를 C 표기법으로 바꾸려면. Rust의 버전은

/* "implementing" the trait */
void bar_int(...) { ... }
void bar_string(...) { ... }

/* the monomorphised `call_bar` function */
void call_bar_int(int value) {
    bar_int(value);
}
void call_bar_string(string value) {
    bar_string(value);
}

int main() {
    call_bar_int(1);
    call_bar_string("foo");
    // pretend that is the (hypothetical) `string` type, not a `char*`
    return 1;
}

Go의 경우 다음과 같습니다.

/* implementing the interface */
void bar_int(...) { ... }
void bar_string(...) { ... }

// the Foo interface type
struct Foo {
    void* data;
    struct FooVTable* vtable;
}
struct FooVTable {
    void (*bar)(void*);
}

void call_bar(struct Foo value) {
    value.vtable.bar(value.data);
}

static struct FooVTable int_vtable = { bar_int };
static struct FooVTable string_vtable = { bar_string };

int main() {
    int* i = malloc(sizeof *i);
    *i = 1;
    struct Foo int_data = { i, &int_vtable };
    call_bar(int_data);

    string* s = malloc(sizeof *s);
    *s = "foo"; // again, pretend the types work
    struct Foo string_data = { s, &string_vtable };
    call_bar(string_data);
}

(이것은 정확히 옳지 않습니다-vtable에 더 많은 정보가 있어야하지만) 여기에 동적 함수 포인터 인 메소드 호출이 관련이 있습니다.

녹은 선택을 제공합니다

다시 돌아 가기

Rust의 접근 방식을 통해 사용자는 정적 디스패치와 동적 디스패치 중에서 선택할 수 있습니다.

지금까지 Rust에 정적으로 전달 된 제네릭이있는 Rust 만 시연했지만 Rust는 특성 객체를 통해 Go와 같은 동적 항목 (기본적으로 동일한 구현)을 선택할 수 있습니다. 특성 &Foo을 구현하는 알 수없는 유형에 대한 빌린 참조 인 처럼 표시 Foo됩니다. 이 값은 Go 인터페이스 객체와 동일 / 매우 유사한 vtable 표현을 갖습니다. (특성 오브젝트는 "존재 유형" 의 예입니다 .)

동적 디스패치가 실제로 도움이되는 경우 (예 : 코드 팽창 / 중복을 줄임으로써 더 성능이 좋은 경우)가 있지만 정적 디스패치를 ​​사용하면 컴파일러가 호출 사이트를 인라인하고 모든 최적화를 적용 할 수 있으므로 일반적으로 더 빠릅니다. 이는 정적 디스패치 특성 메소드 호출을 통해 이러한 반복자가 C 레벨 만큼 빠르면서도 여전히 높은 수준과 표현력으로 보이는 Rust의 반복 프로토콜 과 같은 경우에 특히 중요합니다 .

Tl; dr : Rust의 접근 방식은 프로그래머의 재량에 따라 정적 및 동적 디스패치를 ​​제네릭으로 제공합니다. Go는 동적 디스패치 만 허용합니다.

파라 메트릭 다형성

또한, 특성을 강조하고 반사를 강조하지 않으면 Rust는 훨씬 강력한 파라 메트릭 다형성을 얻을 수 있습니다.

Go의 접근 방식은 매우 유연하지만 함수의 내부에서 추가 유형 정보를 쿼리 할 수 ​​있고 수행 할 수 있기 때문에 호출자에 대한 보장이 적습니다 (프로그래머가 추론하기가 다소 어렵습니다). iirc, 라이터를 사용하는 함수는 리플렉션을 사용 Flush하여 일부 입력 을 호출 하지만 다른 입력 은 호출 하지 않는 표준 라이브러리 )

건물 추상화

이것은 다소 아픈 점이므로 간단히 말하지만 Rust와 같은 "적절한"제네릭을 사용하면 Go map와 같은 낮은 수준의 데이터 유형을 허용 []하고 실제로 표준 라이브러리에서 강력한 형식 안전 방식으로 직접 구현할 수 있습니다. Rust ( HashMapVec각각)로 작성되었습니다 .

그리고 그 유형뿐만 아니라 LruCache해시 맵 위에 일반 캐싱 레이어와 같이 형식이 안전한 일반 구조를 작성할 수 있습니다 . 즉, 사람들은 데이터 를 저장 / 삽입 할 때 데이터를 저장 하거나 형식 명제를 사용 하지 않고도 표준 라이브러리에서 직접 데이터 구조를 사용할 수 있습니다 interface{}. 즉,이있는 경우 LruCache<int, String>키가 항상 int이고 값이 항상 Strings 임을 보장합니다. 실수로 잘못된 값을 삽입하거나 (-가 아닌 추출 시도 String) 방법이 없습니다.


내 자신 AnyMap은 Rust의 강점을 잘 보여주고 있으며, 특성 객체를 제네릭과 결합하여 Go에서 필연적으로 쓰여질 깨지기 쉬운 것을 안전하고 표현 적으로 추상화합니다 map[string]interface{}.
크리스 모건

내가 예상했듯이 Rust는 더 강력하고 기본 / 우아하게 더 많은 선택을 제공하지만 Go의 시스템은 거의 누락 된 것들이와 같은 작은 핵으로 달성 될 수있을 정도로 가깝습니다 interface{}. Rust는 기술적으로 우수 해 보이지만 여전히 Go에 대한 비판이 너무 가혹하다고 생각합니다. 프로그래머의 힘은 99 %의 작업에 필적합니다.
로건

22
Rust가 목표로하는 저수준 / 고성능 도메인을위한 @Logan (예 : 운영 체제, 웹 브라우저 ... 핵심 "시스템"프로그래밍 항목)은 정적 디스패치 (및 제공 / 최적화 성능) 옵션을 갖지 않습니다. 허용되지 않습니다). Go가 이러한 종류의 응용 프로그램에 녹만큼 적합하지 않은 이유 중 하나입니다. 어쨌든 프로그래머의 힘은 실제로 동등하지 않으며 재사용 가능하고 내장되지 않은 데이터 구조에 대한 (컴파일 시간) 유형 안전을 잃어 런타임 유형 어설 션으로 돌아갑니다.
huon

10
정확히 맞습니다. 훨씬 더 많은 힘을 제공합니다. 나는 Rust를 안전한 C ++로 생각하고 빠른 Python (또는 크게 단순화 된 Java)으로 생각합니다. 개발자 생산성이 가장 중요하고 런타임 및 가비지 수집과 같은 문제가 발생하지 않는 많은 작업을 수행하려면 Go (예 : 웹 서버, 동시 시스템, 명령 줄 유틸리티, 사용자 응용 프로그램 등)를 선택하십시오. 모든 마지막 성능이 필요하고 개발자 생산성이 저하되는 경우 Rust (예 : 브라우저, 운영 체제, 리소스 제한 임베디드 시스템)를 선택하십시오.
weberc2
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.