我想在C#中在堆栈上分配一个对象

11

假设我有这个 C# 类:

public class HttpContextEx
{
    public HttpContext context = null;
    public HttpRequest req = null;
    public HttpResponse res = null;
}

如何在函数内声明一个对象,使其分配在栈上而不是堆上?
换句话说,我想避免使用'new'关键字。以下代码是错误的

HttpContextEx ctx = new HttpContextEx(); // << allocates on the heap!

我非常清楚栈和堆是什么,并且听说过C#的优秀垃圾回收机制,但我坚持将这个只是用于方便的小对象分配到栈上。

这种态度来自于C ++(我的主要工具),所以我不能忽略它,我的意思是这真的破坏了我的乐趣(:


5
你也得学会用 C# 思考。 - PostMan
6
@Poni:那么你可能应该只写C#,而不是试图用C#写C++。我其实不太明白问题出在哪里,但C#/.NET被设计成这样是有原因的。 - eldarerathis
3
创建 HttpContextEx 的目的是什么呢?现有的 HttpContext 类已经提供了 RequestResponse 属性。 - John Rasch
2
@John Rasch 当我可以只传递一个好的对象时,我在传递相同的六个指针给12个以上的函数时会变得懒惰。 - Poni
1
简而言之 - 我无法知道这些答案会出现,如果我知道这些就是答案,我就不会问了,对吧?!此外,aqwert的回答才是答案,没有那些“废话”。 - Poni
显示剩余7条评论
5个回答

18

如果您使用struct将其更改为值类型,并在方法体内创建一个新实例,则会在堆栈上创建它。但是,由于成员是引用类型,因此它们仍将位于堆上。无论是值类型还是引用类型,语言仍需要使用new运算符,但您可以使用var来消除类型名称的双重使用。

var ctx = new HttpContextEx(); 

否则,就直接使用C#,因为垃圾回收器做得很好。


1
"但是您可以使用var来消除类型名称的重复使用" << 这个真让我笑了(: 无论如何,非常清晰明了的答案,感谢您的支持! - Poni
只是为了让我能够链接这篇 Eric Lippert 写的好文章 - 把值类型放在堆栈上是当前所有运行时的实现细节,但也可能它们也存储在堆上。 (第二部分) - codekaizen
很好的答案。还可以考虑record struct或者带有字段名称的值元组(Value Tuples)。 - StriplingWarrior

10

你不能(也不应该)那样做。即使你使用struct(会被放在堆栈上),你也必须使用new操作符来处理其中包含的类。说句认真的话,如果你换一种语言,也要换一种态度。


7
使用 ValueType 的派生类来进行栈分配只是当前 CLR 实现的一个产物。在规范中并没有要求它必须这样。 - codekaizen
6
@Poni: 虽然结构体可以很有用,但它们应该是不可变的,并且很小(例如16个字节或更少),因为每次将它们传递到子例程中时,表示对象的整个内存块都会被复制,除非您希望在所有函数参数中使用“ref”。在我五年多的C#开发经验中,我还没有遇到过需要担心.Net堆栈/堆速度的情况。简而言之,我只声明类并让.Net来处理它,而我专注于应用程序。 - Will
2
如果这符合你的兴趣,那好吧 - 我只希望(对你来说)它也能符合你雇主的兴趣。 - Femaref
1
哇,Poni。你愿意承受性能损失来避免C#的最佳实践吗?你知道,在堆栈上分配内存甚至不是你的习惯,而是编译器或运行时的习惯。你只需要声明一个变量,编译器/运行时就会决定把它放在哪里。所以你的意思是...你希望.NET运行时采用你的C++编译器的习惯? - Qwertie
1
@Poni:不幸的是,你在这里发表的最后三条评论表明C++已经对你的心理和关于软件抽象思维能力造成了不可逆转的损害。你给出的栈和堆的定义让我的大脑爆炸了。你对底层实现做出了太多的假设。 - Greg D
显示剩余17条评论

3
你一定是C++出身。在.NET中,事情不是那样的。
所有引用类型都分配在托管堆上,由垃圾回收器跟踪。对于函数作用域内的对象引用,如果函数退出得很快,分配的对象可能仅留在托管堆的第0代中,这会导致非常高效的内存收集。托管堆专门用于处理短生命周期的对象,与你所熟悉的C++堆不同,它甚至没有相同的分配策略。
这就是CLR的工作原理。如果你想以另一种方式工作,请尝试使用非托管运行时。

不,这是不正确的。所有的引用类型都分配在托管堆上。 - Enigmativity
这是在它被写下时的真实情况。不确定你在这里以低努力评论做出了什么贡献。 - codekaizen
在.NET中从来没有这样的说法。大多数对象都分配在堆上,但还有一个与堆分离的大对象堆(Large Object Heap)。编译器可以自由地进行其他优化,可能会将对象分配到其他地方。 - Enigmativity

2
在.NET中,类是引用类型,而结构体是值类型。
如果你真的想让这个类型在堆栈上分配,你可以创建一个结构体。然而,有几个原因不建议这样做:
  • 结构体的实现更加复杂。除非你有足够的理由去创建一个结构体,否则应该使用类。
  • 结构体适用于表示单个单位的类型,而你的类型只是三个独立单位的容器。
  • 结构体的大小不应超过16字节以确保效率。在32位系统上,可以满足该限制,但在64位系统上,结构体的大小超出了该限制。
  • 要作为值类型正常工作,结构体应该是不可变的,而你的类型不是。
最后,将小对象分配到堆上并没有什么本质的坏处。内存管理器实际上是专门设计来高效处理小而短暂的对象的。
下面是一段示例代码,如果它是一个结构体就不能工作,但如果它是一个类就可以很好地工作:
public void SetContext(HttpContextEx ex) {
  ex.context = HttpContext.Current;
  ex.req = ex.context.Request;
  ex.res = es.context.Response;
}

HttpContextEx ctx = new HttpCoontextEx();
SetContext(ctx);

关于这段代码,我理解了。如果我加上'ref'关键字,那么就没问题了,是吗? - Poni
@Poni:你可以使用ref关键字使该代码工作,但是还有其他类似的情况,如果结构没有正确实现,则无法正常工作。 - Guffa

1

简短的回答是,C#不支持您想要做的事情。

您提到的对象是一个类(引用类型),它还包含作为类类型的成员。类实例的内存位于托管堆中,并不支持使用堆栈来存储类实例。
"...引用类型在堆上分配,并进行垃圾回收..."
https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct

但是,您可以将类更改为 ref struct,这样至少其成员的引用将位于堆栈上(尽管成员实例将位于堆上):

public ref struct HttpContextEx
{
    public HttpContext context = null;
    public HttpRequest req = null;
    public HttpResponse res = null;
}

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct


顺便提一下,C# 栈的额外功能:
https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/stackalloc

1
你的回答复杂、曲折、主观、不集中,并且完全偏离了原问题的要点。如果我可以多次投反对票,我会这样做。这个回答太糟糕了。 - Enigmativity
1
using语句不实现"确定性内存回收"。内存的回收方式与任何其他对象完全相同,除非它使用了非托管内存using语句实现了"确定性资源回收",其中资源可以是文件、网络或图形处理程序。 - Enigmativity
1
using语句不实现"确定性内存回收"。内存的回收方式与任何其他对象完全相同,除非它使用任何非托管内存using语句实现了"确定性资源回收",其中资源可以是文件、网络或图形处理程序。 - undefined
1
除非代码的作者明确编写了这样的调用,否则终结器不会调用.Dispose()方法。在对象被回收时,不能假设.Dispose()方法会被调用。您必须检查源代码/文档或始终显式地调用每个可释放对象的.Dispose()方法。 - Enigmativity
Dispose()和Finalize/destruct的概念分离。明白了,谢谢! - Coder
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接