c# - 在結構上使用“新”是否將它分配到堆或棧上?





.net memory-management (8)


幾乎所有被視為值類型的結構都分配在堆棧上,而對象則分配在堆上,而對象引用(指針)則分配到堆棧上。

當你用new操作符創建一個類的實例時,內存將被分配到堆上。 當你使用new運算符創建一個結構實例時,內存分配的位置,堆或堆棧上?




與所有值類型一樣,結構總是在聲明的位置

有關何時使用結構的更多詳細信息,請參閱here的問題。 這裡的這個問題有關結構的更多信息。

編輯:我錯誤地回答說,他們總是在堆棧中。 這是incorrect




包含struct字段的內存可以根據具體情況分配在堆棧或堆上。 如果struct-type變量是某個匿名委託或迭代器類未捕獲的局部變量或參數,則它將被分配到堆棧上。 如果變量是某個類的一部分,那麼它將在堆中的類中分配。

如果結構被分配在堆上,那麼調用new運算符實際上並不是分配內存所必需的。 唯一的目的是根據構造函數中的內容設置字段值。 如果構造函數沒有被調用,那麼所有的字段將得到它們的默認值(0或null)。

同樣,對於在堆棧中分配的結構,除了C#要求所有局部變量在使用前都設置為某個值之外,因此您必須調用自定義構造函數或默認構造函數(不帶參數的構造函數始終可用於結構)。




classstruct聲明就像是用於在運行時創建實例或對象的藍圖。 如果您定義了一個名為Person的classstruct ,Person就是該類型的名稱。 如果聲明並初始化Person類型的變量p,則稱p為Person的對像或實例。 可以創建同一Person類型的多個實例,並且每個實例在其propertiesfields可以具有不同的值。

一個class是一個引用類型。 當class一個對像被創建時,該對像被分配到的變量只保存對該內存的引用。 將對象引用分配給新變量時,新變量引用原始對象。 通過一個變量所做的更改反映在另一個變量中,因為它們都指向相同的數據。

struct是一個值類型。 當一個struct被創建時, struct被分配的變量保存結構的實際數據。 當struct被分配給一個新的變量時,它被複製。 因此新變量和原始變量包含相同數據的兩個獨立副本。 對一個副本所做的更改不會影響其他副本。

通常, classes用於建模更複雜的行為,或用於在創建class對像後修改的數據。 Structs最適合小數據結構,主要包含在創建struct後不打算修改的數據。

更多...




簡而言之,新結構對於結構體來說是錯誤的,稱新結構體只是簡單地稱之為構造函數。 結構的唯一存儲位置是它所定義的位置。

如果它是一個成員變量,它將直接存儲在其中定義的任何變量中,如果它是一個局部變量或參數,則它將存儲在堆棧中。

將它與類進行對比,該類在結構完全存儲的任何地方都有引用,而參考點位於堆的某處。 (成員內,本地/參數堆棧)

它可能有助於查看C ++中的哪些類/結構之間沒有真正的區別。 (在語言中有類似的名字,但它們只是指事物的默認可訪問性)當你調用new時,你得到一個指向堆位置的指針,而如果你有一個非指針引用,它直接存儲在堆棧中,或者在另一個對像中,C#中的ala結構。




好吧,讓我們看看我能否做得更清楚。

首先,Ash是正確的:問題在於分配值類型變量的位置。 這是一個不同的問題 - 答案不僅僅是“在堆疊上”。 它比這更複雜(並且由C#2變得更加複雜)。 我有一篇關於這個話題文章,如果需要的話,將會擴展它,但是我們只處理new運營商。

其次,這一切都取決於你在談論什麼水平。 我正在研究編譯器對源代碼所做的事情,就它創建的IL而言。 JIT編譯器在優化相當多的“邏輯”分配方面會做很多巧妙的事情。

第三,我忽略了泛型,主要是因為我實際上並不知道答案,部分原因是它會讓事情變得複雜。

最後,所有這些都與當前的實施一致。 C#規範沒有詳細說明 - 它實際上是一個實現細節。 有些人認為託管代碼開發人員真的不應該在意。 我不確定我會走多遠,但值得一想的是,一個實際上所有局部變量都在堆上的世界 - 這仍然符合規範。

new運算符在值類型上有兩種不同的情況:可以調用無參數構造函數(例如new Guid() )或有參數的構造函數(例如new Guid(someString) )。 這些產生顯著不同的IL。 要理解為什麼,您需要比較C#和CLI規範:根據C#,所有值類型都有一個無參數構造函數。 根據CLI規範, 沒有值類型具有無參數的構造函數。 (一段時間用反射獲取值類型的構造函數 - 你不會找到無參數的。)

對於C#來說,將“用零初始化一個值”作為構造函數是有道理的,因為它保持了語言的一致性 - 您可以將new(...)視為始終調用構造函數。 CLI有不同的想法是有道理的,因為沒有真正的代碼可以調用 - 當然也沒有類型特定的代碼。

在初始化它之後,您將如何處理該值也會產生影響。 IL用於

Guid localVariable = new Guid(someString);

與用於以下情況的IL不同:

myInstanceOrStaticVariable = new Guid(someString);

另外,如果該值用作中間值,例如方法調用的參數,則事情會再次略有不同。 為了顯示所有這些差異,這裡有一個簡短的測試程序。 它沒有顯示靜態變量和實例變量之間的差異:IL會在stfldstsfld之間不同,但僅此stsfld

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

以下是該類的IL,不包括不相關的位(如nops):

.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
}

正如你所看到的,有很多不同的指令用於調用構造函數:

  • newobj :分配堆棧中的值,調用參數化的構造函數。 用於中間值,例如用於賦值給字段或用作方法參數。
  • call instance :使用已經分配的存儲位置(無論是否在堆棧上)。 這在上面的代碼中用於分配給局部變量。 如果同一個局部變量多次使用幾次new調用賦值,它只是初始化舊值頂部的數據 - 它不會每次分配更多的堆棧空間。
  • initobj :使用已分配的存儲位置並擦除數據。 這用於我們所有的無參數構造函數調用,包括那些分配給局部變量的構造函數。 對於方法調用,有效地引入了一個中間局部變量,其值由initobj

我希望這可以說明這個話題是多麼複雜,同時也讓我們看到了一些亮點。 在某些概念意義上,每一次new分配都會在堆棧中分配空間 - 但正如我們所看到的,即使在IL級別也不是真正發生的事情。 我想強調一個特例。 採取這種方法:

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

這個“邏輯上”有4個堆棧分配 - 一個用於變量,另一個用於三個new調用 - 但實際上(對於該特定代碼)堆棧只分配一次,然後重用相同的存儲位置。

編輯:只是要清楚,這只是在某些情況下是真的...特別是,如果Guid構造函數拋出異常, guid的值將不可見,這就是為什麼C#編譯器能夠重用相同的堆棧插槽。 有關更多詳細信息以及適用的情況,請參閱Eric Lippert 關於價值類型構建博文

我在寫這個答案的過程中學到了很多,如果有任何不清楚的地方,請澄清一下!




我可能在這裡錯過了一些東西,但為什麼我們關心分配?

值類型按值傳遞;),因此不能在與定義的範圍不同的範圍進行變異。 為了能夠改變這個值,你必須添加[ref]關鍵字。

引用類型通過引用傳遞並可以發生變化。

當然,不可變的引用類型字符串是最流行的。

數組佈局/初始化:值類型 - >零內存[名稱,zip] [名稱,zip]引用類型 - >零內存 - > null [ref] [ref]




Dispose模式的目的是提供一種清理託管資源和非託管資源的機制,以及何時發生取決於如何調用Dispose方法。 在你的例子中,Dispose的使用實際上並沒有做與處理有關的任何事情,因為清除列表對處理該集合沒有影響。 同樣,將變量設置為null的調用也不會影響GC。

你可以看看這篇article ,了解如何實現Dispose模式的更多細節,但它基本如下所示:

public class SimpleCleanup : IDisposable
{
    // some fields that require cleanup
    private SafeHandle handle;
    private bool disposed = false; // to detect redundant calls

    public SimpleCleanup()
    {
        this.handle = /*...*/;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Dispose managed resources.
                if (handle != null)
                {
                    handle.Dispose();
                }
            }

            // Dispose unmanaged managed resources.

            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

這裡最重要的方法是Dispose(bool),它實際上在兩種不同的情況下運行:

  • 處置== true:該方法直接或間接由用戶代碼調用。 可以處置託管和非託管資源。
  • 處置== false:該方法已由運行時從終結器中調用,並且不應引用其他對象。 只能處理非託管資源。

簡單地讓GC負責清理的問題是,您無法真正控制GC何時運行一個收集週期(您可以調用GC.Collect(),但實際上不應該這樣做),因此資源可能會停留比需要的時間長。 請記住,調用Dispose()實際上並不會導致收集週期或以任何方式導致GC收集/釋放對象; 它只是提供了更確定地清理所使用的資源的方法,並告訴GC該清理已經執行。

IDisposable的全部和配置模式並不是立即釋放內存。 唯一一次對Dispose的調用實際上甚至有可能立即釋放內存的時間是處理處置== false場景和處理非託管資源的時間。 對於託管代碼,直到GC運行一個你真正無法控制的採集週期(除了調用GC.Collect(),我已經提到這不是一個好主意)之前,內存實際上不會被回收。

你的場景並不真正有效,因為.NET中的字符串沒有使用任何未經處理的資源,也沒有實現IDisposable,所以沒有辦法強制它們被“清理”。





c# .net memory-management