Pas Özellikleri Go Arayüzlerinden ne kadar farklıdır?


64

Go ile tanıştım, içinde birkaç küçük program yazdım. Pas, elbette, daha az aşina olacağım ama göz kulak olacağım.

Son zamanlarda http://yager.io/programming/go.html okuduktan sonra , Generics'in ele alınmasının iki yolunu şahsen inceleyeceğimi düşündüm, çünkü makale, haksız yere eleştirdi gibi görünüyordu. zarifçe başaramadı. Rust'un Özelliklerinin ne kadar güçlü olduğuna dair yutturmaca duydum ve insanlardan Go hakkındaki eleştirilerden başka hiçbir şey duymadım. Go'da biraz tecrübe sahibi olarak, bunun ne kadar doğru olduğunu ve nihayetinde farklılıkların ne olduğunu merak ettim. Bulduğum şey, Özellikler ve Arayüzlerin oldukça benzer olmasıydı! Nihayetinde bir şeyi kaçırıp kaçırmadığımdan emin değilim, bu yüzden benzerliklerinin hızlı bir şekilde öğrenildiği ve böylece neyi özlediğimi söyleyebileceksiniz!

Şimdi Go Interfaces'e belgelerine bakalım :

Go'daki arayüzler bir nesnenin davranışını belirtmenin bir yolunu sağlar: eğer bir şey yapabilirse, o zaman burada kullanılabilir.

En yaygın arayüz, Stringernesneyi temsil eden bir dize döndürendir.

type Stringer interface {
    String() string
}

Yani, String()üzerinde tanımlanmış olan herhangi bir Stringernesne bir nesnedir. Bu, func (s Stringer) print()neredeyse tüm nesneleri alan ve bunları basan tür imzalarında kullanılabilir .

Aynı zamanda interface{}herhangi bir nesneyi alan var. Daha sonra yansıma yoluyla çalışma zamanında türü belirlemeliyiz.


Şimdi, Pas Özelliklerine belgelerine bakalım :

En basit haliyle, özellik sıfır veya daha fazla yöntem imza kümesidir. Örneğin, konsola yazdırılabilecek şeyler için Yazdırılabilir özelliğini tek bir yöntem imzası ile ilan edebiliriz:

trait Printable {
    fn print(&self);
}

Bu hemen Go Arayüzlerimize oldukça benziyor. Gördüğüm tek fark, sadece yöntemleri tanımlamak yerine, Özelliklerin Uygulamalarını tanımlamamızdır. Öyleyse yapıyoruz

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

onun yerine

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

Bonus Soru: Bir özelliği uygulayan ancak kullanmadığınız bir işlevi tanımlarsanız Rust'ta ne olur impl? Sadece çalışmıyor mu?

Go'nun Arayüzlerinden farklı olarak, Rust'un tip sistemi, interface{}derleyici ve çalışma zamanı aslında türü bildiğinde uygun jenerikler ve benzeri şeyler yapmanızı sağlayan tip parametrelerine sahiptir . Örneğin,

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

herhangi bir tür üzerinde çalışır ve derleyici , sıra öğelerinin türünün yansıma kullanmak yerine derleme zamanında olduğunu bilir .


Şimdi, asıl soru: Burada herhangi bir fark eksik mi? Gerçekten o kadar benzer mi? Burada özlediğim daha temel bir fark yok mu? (Kullanımda. Uygulama detayları ilginç, ancak aynı şekilde çalışması durumunda sonuçta önemli değil.)

Sözdizimsel farklılıkların yanı sıra, gördüğüm gerçek farklar:

  1. Go otomatik bir yöntem gönderme yerine, Rust implbir Özelliği uygulamak için gerekli
    • Zarif vs Açık
  2. Rust, yansımasız uygun jeneriklere izin veren tip parametrelerine sahiptir.
    • Go burada gerçekten bir cevap yok. Bu, çok daha güçlü olan tek şey ve sonuçta farklı tipteki imzalarla kopyalama ve yapıştırma yöntemlerinin yerine geçiyor.

Bunlar sadece önemsiz olmayan farklar mı? Öyleyse, görünüşte Go'nun Arayüz / Tip sistemi pratikte algılanan kadar zayıf değildir.

Yanıtlar:


59

Bir özelliği uygulayan ancak impl kullanmıyorsanız bir işlevi tanımlarsanız Rust'ta ne olur? Sadece çalışmıyor mu?

Bu özelliği açıkça uygulamanız gerekir; Eşleşen isim / imza yöntemine sahip olmak, Rust için anlamsız.

Genel çağrı gönderme

Bunlar sadece önemsiz olmayan farklar mı? Öyleyse, görünüşte Go'nun Arayüz / Tip sistemi pratikte algılanan kadar zayıf değildir.

Statik gönderim yapmamak, bazı durumlar için önemli bir performans hedefi olabilir (örneğin, Iteratoraşağıda bahsettiğim gibi). Bence demek istediğin bu

Go burada gerçekten bir cevap yok. Bu, çok daha güçlü olan tek şey ve sonuçta farklı tipteki imzalarla kopyalama ve yapıştırma yöntemlerinin yerine geçiyor.

ama daha detaylı olarak anlatacağım, çünkü farkı derinden anlamaya değer.

Pas içinde

Rust'un yaklaşımı, kullanıcının statik gönderim ve dinamik gönderim arasında seçim yapmasını sağlar . Bir örnek olarak, varsa

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());
}

daha sonra, call_baryukarıdaki iki çağrı sırasıyla sırasıyla çağrıları derler.

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

bu .bar()yöntem çağrıları statik işlev çağrılarıdır, yani bellekteki sabit bir işlev adresine. Bu, satırlama gibi optimizasyonlara izin verir, çünkü derleyici tam olarak hangi fonksiyonun çağrıldığını bilir . (C ++ 'ın yaptığı da budur, bazen "monomorphisation" olarak adlandırılır.)

Go'da

Go sadece "generic" fonksiyonlar için dinamik gönderime izin verir, yani metod adresi değerden yüklenir ve sonra oradan çağrılır, yani tam fonksiyon sadece çalışma zamanında bilinir. Yukarıdaki örneği kullanarak

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"))
}

Şimdi, bu iki call_barler her zaman yukarıdakileri arayacaklar , arayüzün vtable'sinden yüklenen call_baradreslerle .bar

Düşük seviye

Yukarıdakileri tekrar ifade etmek için, C notasyonunda. Rust'un versiyonu yaratır

/* "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 için daha çok şey var:

/* 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);
}

(Bu tam olarak doğru değil --- vtable'da daha fazla bilgi olmalı --- ancak dinamik bir işlev işaretçisi olan yöntem çağrısı burada ilgili şeydir.)

Rust seçenek sunar

Geri dönüyor

Rust yaklaşımı, kullanıcının statik gönderim ve dinamik gönderim arasında seçim yapmasını sağlar.

Şimdiye kadar Rust'a yalnızca statik olarak jenerik jeneriklere sahip olduğunu gösterdim, ancak Rust, özellikle esas olarak aynı uygulama ile Go gibi dinamik olanları, özellik nesneleri aracılığıyla kabul edebilir. Not niteliğinde &Fooolan, Fooözelliği uygulayan bilinmeyen bir türe borçlu bir referanstır . Bu değerler Go arayüz nesnesine aynı / çok benzer değişken gösterime sahiptir. (Bir özellik nesnesi "varoluşsal bir tür" örneğidir .)

Dinamik gönderimin gerçekten yararlı olduğu durumlar vardır (ve bazen kod şişmesi / çoğaltmayı azaltarak, örneğin daha yüksek performans gösterebilir), ancak statik gönderim, derleyicilerin çağrı alanlarını sıralamasına ve tüm optimizasyonlarını uygulamalarına izin verir, yani normalde daha hızlı olur. Bu, statik gönderme özelliği yönteminin çağırdığı Rust'in yineleme protokolü gibi şeyler için özellikle önemlidir , ancak yineleyiciler için C eşdeğerleri kadar hızlı olmalarını sağlarken, yine de yüksek düzeyde ve etkileyici görünmektedir .

Tl; dr: Rust'un yaklaşımı, jeneriklerde, programcıların takdirine bağlı olarak hem statik hem de dinamik gönderim sunar; Git yalnızca dinamik gönderime izin verir.

Parametrik polimorfizm

Ayrıca, özellikleri vurgulamak ve yansımayı ortadan kaldırmak, Rust'a daha güçlü parametrik polimorfizm verir : programcı, bir fonksiyonun argümanları ile tam olarak ne yapabileceğini bilir, çünkü fonksiyon imzasında uygulanan genel tipleri açıklamak zorundadır.

Go'nun yaklaşımı çok esnektir, ancak arayanlar için daha az garantisi vardır (programcının nedenini zorlaştırdığını) çünkü bir işlevin içindekiler ek tip bilgileri sorgulayabilir (Go'da bir hata olmuştur). standart kütüphane, iirc, bir yazarı alan bir fonksiyonun Flushbazı girişleri çağırmak için yansıma kullandığı , diğerlerini değil).

Bina soyutlamaları

Bu biraz sıkıcı bir nokta, bu yüzden sadece kısaca konuşacağım, ancak Rust gibi "uygun" bir jenerikliğe sahip olmak, Go'lar gibi düşük seviyeli veri türlerine mapve []aslında doğrudan standart kütüphanede güçlü bir şekilde güvenli bir şekilde uygulanmasına olanak sağlıyor ve Rust ( HashMapve Vecsırasıyla) ile yazılmıştır .

Ve sadece bu tipler için değil, üstüne güvenli tipte jenerik yapılar da kurabilirsiniz, örneğin LruCachebir hashmap'in üstüne genel bir önbellekleme katmanıdır. Bu, insanların veri yapılarını doğrudan standart kütüphaneden kullanabileceği, veri saklamaya gerek kalmadan interface{}ve ekleme / çıkartma sırasında tip iddiaları kullanabildiği anlamına gelir . Başka bir deyişle, bir LruCache<int, String>anahtarınız varsa, anahtarların her zaman ints ve değerlerin her zaman Strings olduğundan emin olursunuz : yanlışlıkla yanlış bir değer eklemenin (veya olmayanları çıkarmaya çalışmanın String) hiçbir yolu yoktur .


Benim AnyMapkendim, Go'nun zorunlulukta yazabileceği kırılgan bir şeyin güvenli ve etkileyici bir soyutlamasını sağlamak için özellik nesnelerini jeneriklerle birleştiren Rust'in güçlü yönlerinin iyi bir göstergesidir map[string]interface{}.
Chris Morgan

Beklediğim gibi, Rust daha güçlü ve doğal / zarif bir şekilde daha fazla seçenek sunuyor, ancak Go'nun sistemi özlediği çoğu şeyi küçük kesicilerle gerçekleştirebilecek kadar yakın interface{}. Rust teknik olarak üstün görünse de, Go'nun eleştirisinin ... biraz zor olduğunu düşünüyorum. Programcının gücü, görevlerin% 99'u için hemen hemen aynıdır.
Logan,

22
@Logan, düşük seviyeli / yüksek performanslı alanlar için Rust, (örneğin işletim sistemleri, web tarayıcıları ... temel "sistemler" programlama öğeleri) statik statik gönderme seçeneğine sahip olmayan (ve sağladığı performans / optimizasyon) hedefliyor izin verir) kabul edilemez. Go'nun bu tür uygulamalar için Rust kadar uygun olmama nedenlerinden biri. Her durumda, programcının gücü gerçekten aynı değildir, yeniden kullanılabilir ve yerleşik olmayan herhangi bir veri yapısı için çalışma zamanı türü iddialarına geri dönerek (derleme zamanı) tür güvenliğini kaybedersiniz.
huon

10
Bu kesinlikle doğru - Rust size çok daha fazla güç sunuyor. Rust'u güvenli bir C ++ olarak düşünüyorum ve hızlı bir Python (veya çok basitleştirilmiş bir Java) olarak gidin. Geliştirici verimliliğinin en önemli olduğu görevlerin (ve çalışma süreleri ve çöp toplama gibi şeylerin sorunlu olmadığı) büyük bir yüzdesi için Git'i seçin (örneğin, web sunucuları, eşzamanlı sistemler, komut satırı programları, kullanıcı uygulamaları vb.). Her son performansa ihtiyaç duyuyorsanız (ve geliştiricinin verimliliği artarsa), Rust'u seçin (ör. Tarayıcılar, işletim sistemleri, kaynaklarla sınırlı gömülü sistemler).
weberc2
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.