C yapılarını parametre ile mi yoksa geri dönüş değeriyle mi başlatmalıyım? [kapalı]


33

Çalıştığım şirket, tüm veri yapılarını şöyle bir ilkleme işlevi ile başlatıyor:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

Yapılarımı bu şekilde başlatmaya çalışırken bir miktar geri kazandım:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

Birinin diğerine göre üstünlüğü var mı?
Hangisini yapmalıyım ve daha iyi bir uygulama olarak nasıl haklı gösterebilirim?


4
@gnat Yapısal başlatma ile ilgili açık bir soru. Bu iş parçacığı, bu özel tasarım kararı için uygulandığını görmek istediğim aynı mantığı içeriyor.
Trevor Hickey

2
@Jefffrey C'deyiz, bu yüzden aslında yöntemlerimiz olamaz. Her zaman doğrudan bir değerler kümesi değildir. Bazen bir yapıyı başlatmak değerleri (bir şekilde) almak ve yapıyı başlatmak için bir mantık gerçekleştirmektir.
Trevor Hickey,

1
@JacquesB anladım "İnşa ettiğiniz her bileşen diğerlerinden farklı olacaktır. Yapı için başka bir yerde kullanılan bir Initialize () işlevi var. Teknik olarak konuşursak, kurucu olarak adlandırmak yanıltıcıdır."
Trevor Hickey,

1
@TrevorHickey InitializeFoo()bir yapıcıdır. Bir C ++ yapıcısından tek farkı, thisişaretçinin örtük olarak değil açıkça belirtilmesidir. Derlenmiş kod InitializeFoo()ve karşılık gelen bir C ++ Foo::Foo()tamamen aynıdır.
cmaster

2
Daha iyi seçenek: C ++ 'dan C' yi kullanmayı bırakın. Autowin.
Thomas Eding,

Yanıtlar:


25

2. yaklaşımda asla yarı-başlatılmış bir Foo'nuz olmayacak. Tüm inşaatı tek bir yere koymak daha mantıklı ve açık bir yer gibi görünüyor.

Ama ... 1. yol o kadar da kötü değil ve çoğu zaman da kullanılıyor (bağımlılık enjekte etmenin en iyi yolunun, ya birinci yolunuz gibi mülk enjeksiyonunun ya da 2. yol gibi yapıcı enjeksiyonunun tartışılması bile var) . İkisi de yanlış değil.

Öyleyse, ikisi de yanlış değilse ve şirketin geri kalanı # 1 yaklaşımını kullanıyorsa, o zaman mevcut kod tabanına uymalı ve yeni bir kalıp tanıtarak karıştırmaya çalışmamalısınız. Buradaki oyundaki en önemli faktör bu, yeni arkadaşlarınızla iyi oynadığınız ve farklı şeyler yapan o özel kar tanesi olmaya çalışmayın.


Tamam, makul görünüyor. Ne tür bir girdiyi başlattığını görmeden bir nesneyi başlatmanın karışıklığa yol açacağı izlenimini edindim. Öngörülebilir ve test edilebilir kod üretmek için veri giriş / veri çıkışını izlemeye çalışıyordum. Bunu yapmanın diğer yolu, benim yapıma ait kaynak dosyasının başlangıç ​​durumuna getirilmesi için fazladan bağımlılık gerektirmesi nedeniyle eşleşmeyi arttırmış gibi görünüyordu. Yine de haklısın, bu yüzden bir yol diğerinden daha fazla tercih edilmedikçe tekneyi sallamak istemiyorum.
Trevor Hickey,

4
@TrevorHickey: Aslında verdiğiniz örnekler arasında iki temel fark olduğunu söyleyebilirim . (2) Birinde başlatma parametreleri işleve iletilir, diğeri ise örtüktür. (2) hakkında daha fazla soru soruyor gibisin, ama buradaki cevaplar (1) 'e odaklanıyor. Bunu açıklığa kavuşturmak isteyebilirsiniz - çoğu insanın açık parametreler ve bir işaretçi kullanarak ikisinin bir melezini tavsiye edeceğinden şüpheleniyorum:void SetupFoo(Foo *out, int a, int b, int c)
psmears

1
İlk yaklaşım "yarı başlatılmış" bir Fooyapıya nasıl yol açar ? İlk yaklaşım aynı zamanda tüm başlatma işlemlerini tek bir yerde gerçekleştirir. (Veya bir değerlendiriyorlar un başlatıldı Foo"yarı-başlatıldı" olarak struct?)
jamesdlin

1
Foo'nun yaratıldığı durumlarda @jamesdlin ve InitialiseFoo kazayla özlüyor. Uzun bir açıklama yazmadan 2 aşamalı başlatma işlemini tanımlamak sadece bir konuşma şekliydi. Tecrübeli geliştirici tipi insanların anlayacağını düşündüm.
gbjbaanb

22

Her iki yaklaşım da başlatma kodunu tek bir işlev çağrısında toparlar. Çok uzak çok iyi.

Ancak, ikinci yaklaşımla iki konu var:

  1. İkincisi sonuçta ortaya çıkan nesneyi yapmaz, yığında başka bir nesneyi başlatır ve daha sonra nihai nesneye kopyalanır. Bu yüzden ikinci yaklaşımı biraz aşağı görecektim. Aldığınız geri bildirim büyük olasılıkla bu yabancı kopyadan kaynaklanıyor.

    Bir sınıfı türetmek, bu da kötü Derivedgelen Foo(struct'lar büyük ölçüde C nesne yönelimi için kullanılır): İkinci yaklaşımda, fonksiyon ConstructDerived()çağırır ConstructFoo(), ortaya çıkan geçici kopya Foobir üst sınıf yuvasına üzerinde nesneyi Derivednesne; Derivednesnenin başlatılmasını bitir ; yalnızca elde edilen nesnenin iade edildiğinde tekrar kopyalanmasını sağlamak. Üçüncü bir katman ekleyin ve her şey tamamen saçma olur.

  2. İkinci yaklaşımla, ConstructClass()işlevler yapım aşamasında olan nesnenin adresine erişemez. Bu, yapım sırasında nesneleri birbirine bağlamayı imkansız kılar, çünkü bir nesnenin bir geri çağırma için kendisini başka bir nesneye kaydetmesi gerektiğinde gereklidir.


Son olarak, hepsi structstam teşekküllü sınıf değildir. Bazıları structs, bu değişkenlerin değerlerine herhangi bir iç kısıtlama getirmeden, bir sürü değişkeni bir araya getirir. typedef struct Point { int x, y; } Point;buna iyi bir örnek olur. Bir başlatıcı işlevinin bu kullanımı için fazlaca görünüyor. Bu durumlarda, bileşik değişmez sözdizimi uygun olabilir (C99’dur):

Point = { .x = 7, .y = 9 };

veya

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}


5
Derleyicinin kopyayı seçebilmesi kopyayı yazdığınız gerçeğini hafifletmez. C’de gereksiz işlemler yazmak ve bunları düzeltmek için derleyiciye güvenmek kötü kural olarak kabul edilir. Bu, derleyicinin yuvalanmış şablonlarının bıraktığı tüm boşlukları teorik olarak çıkarabildiğini kanıtlayabildiklerinde insanların gurur duyduğu C ++ 'dan farklıdır. C'de insanlar tam olarak makinenin çalışması gereken kodu yazmaya çalışırlar. Neyse, erişilemeyen adreslerle ilgili nokta kalır, kopya seçimi size orada yardımcı olamaz.
cmaster

3
Bir derleyici kullanan herkes yazdığı kodun derleyici tarafından değiştirilmesini beklemelidir. Bir donanım C yorumlayıcısı çalıştırmıyorlarsa, yazdıkları kod, başka türlü inanmak kolay olsalar bile yürüttükleri kod olmayacaktır. Derleyicilerini anlarlarsa, seçimleri anlarlar int x = 3;ve dizgiyi binary'de saklamaktan farklı değildir x. Adres ve miras puanları iyidir; varsayılan elüsyon başarısızlığının aptalca olduğu kabul edilir.
Yakk

@Yakk: Tarihsel olarak, C, sistem programlaması için bir üst seviye meclis dili olarak hizmet etmek için icat edildi. O zamandan beri, kimliği giderek daha bulanık hale geldi. Bazı insanlar, bunun optimize edilmiş bir uygulama dili olmasını ister, ancak daha üst düzey bir montaj dili biçiminin daha iyi bir şekilde ortaya çıkmaması nedeniyle, bu ikinci role hizmet etmek için C'ye hala ihtiyaç vardır. İyi yazılmış bir program kodunun, en az optimizasyonla derlenmiş olsa bile en azından düzgün davranması gerektiği fikrinde yanlış bir şey görmüyorum, ancak gerçekten işe yaraması için C'nin uzun süredir sahip olduğu bazı şeyler eklemesini gerektiriyor.
supercat

@Yakk: Örneğin, bir derleyiciye "Aşağıdaki değişkenler aşağıdaki kod genişletme sırasında güvenli bir şekilde tutulabilir" gibi bir derleyiciye sahip olacak ve bunun yanı sıra unsigned char, optimizasyona izin verecek olandan farklı bir türü blok kopyalama aracı Kesin Aliasing Kuralı yetersiz kalacak ve aynı zamanda programcının beklentilerini de netleştirecek.
supercat

1

Yapının içeriğine ve kullanılan belirli bir derleyiciye bağlı olarak, her iki yaklaşım da daha hızlı olabilir. Tipik bir örnek, belirli kriterleri karşılayan yapıların kayıtlara geri alınabilmesidir; diğer yapı türlerini döndüren işlevler için arayan kişinin geçici yapı için bir yere yer ayırması (tipik olarak yığında) ve adresini "gizli" bir parametre olarak iletmesi gerekir; bir işlevin dönüşünün doğrudan, adresi herhangi bir dış kod tarafından tutulmayan bir yerel değişkene depolandığı durumlarda, bazı derleyiciler bu değişkenin adresini doğrudan iletebilir.

Bir yapı tipi, bir fonksiyon dönüşüne sahip olan kayıtlara (örneğin bir makine kelimesinden daha büyük olmayan veya tam olarak iki makine kelimesini doldurarak) geri verilmesi için belirli bir uygulamanın gereksinimlerini karşılarsa yapı, özellikle bir yapının adresini iletmekten daha hızlı olabilir. çünkü bir değişkenin adresini bir kopyasını tutabilecek dış koda maruz bırakmak bazı yararlı optimizasyonları engelleyebilir. Bir tür bu gereksinimleri karşılamıyorsa, yapıyı döndüren bir işlev için oluşturulan kod, bir hedef işaretçiyi kabul eden bir işlev için aynı olacaktır; çağıran kod, imleci alan form için daha hızlı olur, ancak bu form bazı optimizasyon fırsatlarını kaybeder.

C'nin çok kötü olması, bir işlevin, bir işaretleyicinin bir kopyasının (C ++ referansına benzer bir anlamsal) bir kopyasının saklanmasının yasak olduğunu söylemenin bir yolunu sağlamaz; önceden var olan bir nesneye bir işaretçi, ancak aynı zamanda bir derleyicinin bir değişkenin adresini "maruz bıraktığını" dikkate almasını gerektiren anlamsal maliyetlerden kaçının.


3
Son noktaya kadar: C ++ 'da bir fonksiyonun referans olarak geçen bir işaretçinin kopyasını saklamasını engelleyecek hiçbir şey yoktur, fonksiyon basitçe nesnenin adresini alabilir. Ayrıca, bu referansı içeren başka bir nesne oluşturmak için referans kullanmak ücretsizdir (çıplak işaretçi oluşturulmaz). Bununla birlikte, işaretçi kopyası veya nesnedeki referans, işaret ettiği nesneyi aşarak, sarkan bir işaretçi / referans oluşturabilir. Yani referans güvenliği ile ilgili konu oldukça sessiz.
cmaster

@cmaster: Bir işaretçiyi geçici depolamaya geçirerek yapıları döndüren platformlarda, C derleyicileri çağrılan işlevleri, bu depolamanın adresine erişme biçimini sağlamaz. C ++ 'da referans tarafından geçen bir değişkenin adresini bulmak mümkündür, ancak arayan kişi geçen öğenin ömrünü garanti etmediği sürece (bu durumda genellikle bir göstericiyi geçecekti) Undefined Behavior muhtemelen sonuç verir.
supercat

1

"Output-parametresi" stilinin lehine olan bir argüman, fonksiyonun bir hata kodu döndürmesini sağlamasıdır.

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

Bazı ilgili yapılar kümesi göz önüne alındığında, herhangi biri için başlatma işlemi başarısız olursa, hepsinin tutarlılık uğruna parametre dışı stili kullanmaları faydalı olabilir.


1
Biri yeni başlasa da errno.
Deduplicator

0

Odaklanmanızın, yapı argümanlarının nasıl sağlandığındaki tutarsızlığa değil, çıktı parametresine karşı başlatmaya ve geri dönüşe göre başlatmaya başladığını düşünüyorum.

İlk yaklaşımın Fooopak olmasına izin verebileceğini unutmayın (şu anda kullanma şekliniz olmasa da) ve bu genellikle uzun süreli bakım için istenir. Örneğin, Foobaşlatılmadan opak bir yapı atanan bir işlevi düşünebilirsiniz . Belki de Foodaha önce farklı değerlerle başlatılmış bir yapıyı yeniden başlatmanız gerekir .


Downvoter, açıklamak ister misin? Söylediğim bir şey aslında yanlış mı?
jamesdlin
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.