Bir yapıda “yeni” kullanmak onu yığın veya yığına ayırır mı?


290

newİşleç ile bir sınıf örneği oluşturduğunuzda , bellek yığın üzerinde ayrılır. newOperatöre bellek yığınının, yığının veya yığının nereden ayrıldığı bir yapı örneği oluşturduğunuzda ?

Yanıtlar:


306

Tamam, bakalım bunu daha açık hale getirebilir miyim.

İlk olarak, Ash haklıdır: soru, değer tipi değişkenlerinin nereye tahsis edildiği ile ilgili değildir . Bu farklı bir soru - ve cevabın sadece “yığın üzerinde” olmadığı bir soru. Bundan daha karmaşık (ve C # 2 ile daha da karmaşık hale geldi). Bir var konuda makale ve istenirse üzerine genişletmek, ama sadece beraber anlaşma sağlayacak operatörü.new

İkincisi, tüm bunlar gerçekten hangi seviyeden bahsettiğinize bağlıdır. Derleyicinin kaynak koduyla ne yaptığını, oluşturduğu IL açısından bakıyorum. JIT derleyicisinin oldukça fazla "mantıksal" tahsisi optimize etmek açısından akıllıca şeyler yapması mümkün.

Üçüncüsü, jenerikleri görmezden geliyorum, çoğunlukla cevabı gerçekten bilmediğim için ve kısmen de işleri çok fazla karmaşıklaştıracağı için.

Son olarak, tüm bunlar sadece mevcut uygulama ile. C # spec bunun çoğunu belirtmez - etkili bir şekilde bir uygulama detayıdır. Yönetilen kod geliştiricilerinin gerçekten umursamaması gerektiğine inananlar var. O kadar ileri gideceğime emin değilim, ama aslında tüm yerel değişkenlerin öbek üzerinde yaşadığı bir dünyayı hayal etmeye değer - ki bu hala spesifikasyona uygun.


newİşleç ile değer türlerinde iki farklı durum vardır: parametresiz bir kurucu (örn. new Guid()) Veya parameterful bir kurucu (örn. new Guid(someString)) Çağırabilirsiniz . Bunlar önemli ölçüde farklı IL üretir. Nedenini anlamak için, C # ve CLI özelliklerini karşılaştırmanız gerekir: C # 'a göre, tüm değer türlerinin parametresiz bir yapıcısı vardır. CLI spesifikasyonuna göre, hiçbir değer türünün parametresiz yapıcıları yoktur. (Değer türünün yapıcılarını bir süre yansımayla getirin - parametresiz bir tane bulamazsınız.)

O dil tutarlı tutar, çünkü C # yapıcı olarak "sıfır ile bir değer başlatmak" tedavi etmek için mantıklı - aklınıza gelebilecek new(...)olarak her zaman bir kurucu çağırmak. Aramak için gerçek bir kod olmadığı ve kesinlikle türe özgü bir kod olmadığı için CLI'nin farklı düşünmesi mantıklıdır.

Ayrıca, başlattıktan sonra değerle ne yapacağınızı da fark eder. Kullanılan IL

Guid localVariable = new Guid(someString);

aşağıdakiler için kullanılan IL'den farklıdır:

myInstanceOrStaticVariable = new Guid(someString);

Buna ek olarak, değer bir ara değer olarak kullanılırsa, örneğin bir yöntem çağrısının bağımsız değişkeni, işler yeniden biraz farklıdır. Tüm bu farklılıkları göstermek için, kısa bir test programı. Statik değişkenler ve örnek değişkenler arasındaki farkı göstermez: IL stfldve arasında farklılık gösterir stsfld, ama hepsi bu.

using System;

public class Test
{
    static Guid field;

    static void Main() {}
    static void MethodTakingGuid(Guid guid) {}


    static void ParameterisedCtorAssignToField()
    {
        field = new Guid("");
    }

    static void ParameterisedCtorAssignToLocal()
    {
        Guid local = new Guid("");
        // Force the value to be used
        local.ToString();
    }

    static void ParameterisedCtorCallMethod()
    {
        MethodTakingGuid(new Guid(""));
    }

    static void ParameterlessCtorAssignToField()
    {
        field = new Guid();
    }

    static void ParameterlessCtorAssignToLocal()
    {
        Guid local = new Guid();
        // Force the value to be used
        local.ToString();
    }

    static void ParameterlessCtorCallMethod()
    {
        MethodTakingGuid(new Guid());
    }
}

Alakasız bitler (nops gibi) hariç olmak üzere sınıf için IL:

.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object    
{
    // Removed Test's constructor, Main, and MethodTakingGuid.

    .method private hidebysig static void ParameterisedCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
        L_0010: ret     
    }

    .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
    {
        .maxstack 2
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid    
        L_0003: ldstr ""    
        L_0008: call instance void [mscorlib]System.Guid::.ctor(string)    
        // Removed ToString() call
        L_001c: ret
    }

    .method private hidebysig static void ParameterisedCtorCallMethod() cil  managed    
    {   
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0011: ret     
    }

    .method private hidebysig static void ParameterlessCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
        L_0006: initobj [mscorlib]System.Guid
        L_000c: ret 
    }

    .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        // Removed ToString() call
        L_0017: ret 
    }

    .method private hidebysig static void ParameterlessCtorCallMethod() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        L_0009: ldloc.0 
        L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0010: ret 
    }

    .field private static valuetype [mscorlib]System.Guid field
}

Gördüğünüz gibi, yapıcıyı çağırmak için kullanılan birçok farklı talimat vardır:

  • newobj: Yığındaki değeri atar, parametreli bir kurucu çağırır. Ara değerler için kullanılır, örneğin bir alana atama veya yöntem bağımsız değişkeni olarak kullanma.
  • call instance: Önceden ayrılmış bir depolama yeri kullanır (istifte olsun olmasın). Bu, yukarıdaki kodda yerel bir değişkene atamak için kullanılır. Aynı yerel değişken birkaç kullanarak bir değerini birkaç kez atanmışsa newaramaları, sadece eski değerin üstüne üzerinden veri başlatır - bu değil fazla yığın alanı her zaman ayırmak.
  • initobj: Önceden ayrılmış bir depolama yeri kullanır ve verileri siler. Bu, yerel bir değişkene atananlar dahil, parametresiz tüm yapıcı çağrılarımız için kullanılır. Yöntem çağrısı için, bir ara yerel değişken etkili bir şekilde tanıtılır ve değeri tarafından silinir initobj.

Umarım bu, konunun ne kadar karmaşık olduğunu gösterir, aynı zamanda biraz ışık tutar. Gelen bazı kavramsal duyular, her çağrı newyığını üzerinde ayırdığı alanı - ama biz gördüğümüz gibi, gerçekten hatta IL düzeyinde olur şey değildir. Belirli bir vakayı vurgulamak istiyorum. Bu yöntemi kullanın:

void HowManyStackAllocations()
{
    Guid guid = new Guid();
    // [...] Use guid
    guid = new Guid(someBytes);
    // [...] Use guid
    guid = new Guid(someString);
    // [...] Use guid
}

Bu "mantıksal olarak" 4 yığın tahsisine sahiptir - biri değişken için, diğeri üç newçağrının her biri için - ancak aslında (bu belirli kod için) yığın yalnızca bir kez tahsis edilir ve aynı depolama alanı yeniden kullanılır.

DÜZENLEME: Sadece açık guidolmak gerekirse , bu sadece bazı durumlarda doğrudur ... özellikle, Guidyapıcı bir istisna atarsa , görünür olmaz , bu nedenle C # derleyicisi aynı yığın yuvasını yeniden kullanabilir. Daha fazla ayrıntı ve geçerli olmadığı bir vaka için Eric Lippert'in değer türü yapımıyla ilgili blog yayınına bakın .

Bu cevabı yazarken çok şey öğrendim - lütfen herhangi bir sorunun net olup olmadığını belirtin!


1
Jon, HowManyStackAllocations örnek kodu iyi. Ancak, bunu Guid yerine bir Struct kullanmak için değiştirebilir veya yeni bir Struct örneği ekleyebilirsiniz. Sanırım bu doğrudan @ kedar'ın orijinal sorusunu ele alacaktı.
Kül

9
Guid zaten bir yapı. Bkz. Msdn.microsoft.com/en-us/library/system.guid.aspx Bu soru için bir referans türü
seçmezdim

1
Bu 3 adete sahip olduğunuzda List<Guid>ve ona eklediğinizde ne olur ? Bu 3 tahsis (aynı IL) olurdu? Ama büyülü bir yerde tutulurlar
Arec Barrwin

1
@Ani: Eric'in örneğinin bir try / catch bloğu olduğu gerçeğini kaçırıyorsunuz - bu yüzden yapı kurucusu sırasında bir istisna atılırsa, kurucudan önceki değeri görebilmeniz gerekir. Örneğim böyle bir duruma sahip değil - kurucu bir istisna dışında başarısız olursa, zaten görünmeyeceği için değerinin guidsadece üzerine yazılıp yazılmadığı önemli değildir.
Jon Skeet

2
@Ani: Aslında, Eric bunu yazısının sonuna doğru şöyle çağırıyor: "Şimdi, Wesner'ın anlamı ne olacak? Evet, aslında bir yığın tahsisli yerel değişken (ve bir kapanıştaki alan değil) yapıcı çağrısı ile aynı düzeyde "deneyin" iç içe geçtikten sonra, yeni bir geçici hale getirme, geçici başlatma işlemini başlatma ve yerel olarak kopyalama konusunda bu zorluktan geçmiyoruz. Bu özel (ve ortak) durumda optimize edebiliriz Geçici ve kopyanın oluşturulması çünkü bir C # programının farkı gözlemlemesi imkansız! "
Jon Skeet

40

Bir yapının alanlarını içeren bellek, koşullara bağlı olarak yığın veya yığın üzerinde tahsis edilebilir. Struct-type değişkeni, bazı anonim delege veya yineleyici sınıfı tarafından yakalanmayan yerel bir değişken veya parametre ise, yığına atanır. Değişken bir sınıfın parçasıysa, yığın içindeki sınıf içinde tahsis edilecektir.

Yapı yığın üzerinde tahsis edilirse, bellek ayırmak için yeni işleci çağırmak aslında gerekli değildir. Tek amaç, alan değerlerini yapıcıda ne olduğuna göre ayarlamaktır. Yapıcı çağrılmazsa, tüm alanlar varsayılan değerlerini alır (0 veya null).

Yığın üzerinde tahsis edilen yapılar için benzer şekilde, C # öğesinin kullanılmadan önce tüm yerel değişkenlerin bir değere ayarlanmasını gerektirmesi dışında, özel bir kurucu veya varsayılan kurucu (parametre almayan bir kurucu her zaman kullanılabilir yapılar).


13

Kompakt olarak ifade etmek gerekirse, yeni yapılar için bir yanlış adlandırmadır, yeni çağırmak sadece yapıcıyı çağırır. Yapı için tek depolama konumu tanımlandığı konumdur.

Bir üye değişkeni ise, içinde tanımlandığı her şeyde doğrudan saklanır, yerel bir değişken veya parametre ise yığına kaydedilir.

Bunu, yapının bütünüyle saklandığı her yerde bir referansı olan sınıflarla karşılaştırın; referans ise yığın üzerinde bir yere işaret eder. (Üye içindeki yerel / yığındaki parametre)

Sınıf / yapı arasında gerçek bir ayrımın olmadığı C ++ 'a biraz bakmanıza yardımcı olabilir. (Dilde benzer adlar vardır, ancak yalnızca şeylerin varsayılan erişilebilirliğine atıfta bulunurlar) Yeni aradığınızda, yığın konumuna bir işaretçi alırsınız, işaretçi olmayan bir referansınız varsa doğrudan yığına veya diğer nesne içinde ala C # 'da yapılandırır.


5

Tüm değer türlerinde olduğu gibi, yapılar her zaman bildirildikleri yere gider .

Bu soruya bakın burada yapılar ne zaman kullanılacağı hakkında daha fazla ayrıntı için. Ve bu soru burada yapılar hakkında daha fazla bilgi için.

Düzenleme: Ben her zaman yığına gitmek onlar mistankely cevap vardı . Bu yanlış .


"Yapılar her zaman bildirildikleri yere giderler", bu biraz yanıltıcı kafa karıştırıcıdır. Sınıftaki bir yapı alanı her zaman "türün bir örneği oluşturulduğunda dinamik belleğe" yerleştirilir - Jeff Richter. Bu dolaylı olarak öbek üzerinde olabilir, ancak normal bir referans türü ile aynı değildir.
Ash

Hayır, bence tam olarak doğru - bir referans türü ile aynı olmasa bile. Değişkenin değeri, bildirildiği yerde yaşar. Bir referans türü değişkeninin değeri, gerçek veriler yerine bir referanstır, hepsi bu.
Jon Skeet

Özetle, bir yöntemde herhangi bir yerde bir değer türü oluşturduğunuzda (bildirdiğinizde) her zaman yığın üzerinde oluşturulur.
Ash

2
Jon, fikrimi özledin. Bu sorunun ilk sorulmasının nedeni, oluşturmak için yeni operatörü kullanırsanız bir yapının tahsis edildiği birçok geliştiriciye (CLR Via C # okunana kadar dahil) açık olmamasıdır. "Yapılar her zaman bildirildikleri yere git" demek açık bir cevap değildir.
Ash

1
@Ash: Zamanım olursa işe geldiğimde bir cevap yazmaya çalışacağım. Yine de trende kapsayacak denemek için çok büyük bir konu :)
Jon Skeet

4

Muhtemelen burada bir şey kaçırıyorum ama tahsisi neden önemsiyoruz?

Değer türleri değere göre aktarılır;) ve dolayısıyla tanımlandıkları yerden farklı bir kapsamda mutasyona uğratılamazlar. Değeri değiştirebilmek için [ref] anahtar sözcüğünü eklemeniz gerekir.

Referans tipleri referans ile geçirilir ve mutasyona uğrayabilir.

Elbette en popüler olan değişmez referans türleri dizeleri vardır.

Dizi düzeni / başlatma: Değer türleri -> sıfır bellek [ad, zip] [ad, zip] Referans türleri -> sıfır bellek -> boş [ref] [ref]


3
Referans türleri referansla iletilmez - referanslar değere göre iletilir. Bu çok farklı.
Jon Skeet

2

A classveya structbildirim, çalışma zamanında örnekler veya nesneler oluşturmak için kullanılan bir taslak gibidir. Eğer bir tanımlarsanız classveya structKişi denilen Kişi tip adıdır. Kişi türünde bir p değişkenini bildirir ve başlatırsanız, p'nin Kişi nesnesi veya örneği olduğu söylenir. Aynı Kişi tip birden çok örneği oluşturulabilir ve her bir örnek onun farklı değerlere sahip olabilir propertiesve fields.

A classbir referans türüdür. Öğesinin bir nesnesi classoluşturulduğunda, nesnenin atandığı değişken yalnızca bu belleğe bir başvuru içerir. Nesne başvurusu yeni bir değişkene atandığında, yeni değişken orijinal nesneyi ifade eder. Bir değişken üzerinden yapılan değişiklikler diğer değişkene yansıtılır çünkü ikisi de aynı verilere karşılık gelir.

A structbir değer türüdür. A structoluşturulduğunda, structatandığı değişken yapının gerçek verilerini tutar. Ne zaman structyeni bir değişkene atanır, bu kopyalanır. Bu nedenle yeni değişken ve orijinal değişken aynı verilerin iki ayrı kopyasını içerir. Bir kopyada yapılan değişiklikler diğer kopyayı etkilemez.

Genel olarak, classesdaha karmaşık davranışları veya bir classnesne oluşturulduktan sonra değiştirilmesi amaçlanan verileri modellemek için kullanılır . Structsen çok struct, oluşturulduktan sonra değiştirilmesi amaçlanmayan verileri içeren küçük veri yapıları için uygundur .

daha fazlası için...


1

Hemen hemen Değer türleri olarak kabul edilen yapılar yığına ayrılırken, nesneler yığın üzerinde, nesne başvurusu (işaretçi) yığına ayrılır.


1

Yapılar yığına ayrılır. İşte size yardımcı bir açıklama:

yapılar

Ayrıca, .NET içinde örnekleme yapıldığında sınıflar bellekte yığın veya .NET'in ayrılmış bellek alanına ayrılır. Oysa yapılar, yığındaki tahsis nedeniyle somutlaştırıldığında daha fazla verim sağlar. Ayrıca, yapılar içindeki geçiş parametrelerinin değere göre yapıldığına dikkat edilmelidir.


5
Bu, bir yapı bir sınıfın parçası olduğunda ve bu noktada nesnenin geri kalanıyla yığın üzerinde yaşadığı durumu kapsamaz.
Jon Skeet

1
Evet ama aslında sorulan soruya odaklanıyor ve cevaplıyor. Oy verildi.
Ash

... hala yanlış ve yanıltıcı olsa da. Üzgünüz, ama bu sorunun kısa cevapları yok - tek cevap Jeffrey's.
Marc Gravell
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.