我最喜欢的面试问题
您知道值类型和引用类型之间的区别吗?您确定吗?尝试使用这段代码片段来找出您是否真的知道——然后阅读下面的解释以了解更多信息。
引言
您是否曾经见过包含“ref
”参数的函数签名?我敢肯定您见过,但您遇到过将类实例作为“ref
”参数传递给函数的情况吗?由于(在 C# 中)类本身在定义上就是一种引用类型,因此这很容易看出该工程师不理解“值”类型和“引用”类型之间的区别。
背景
在过去,“C”语言引入了“指针”的概念。C# 中的引用类型的工作方式类似于“C”中的指针。那么什么是指针,它们是如何工作的呢?好吧,使用“C”语言,我们过去这样写:
int value = 15;
int *p = &value;
在上面的语句中,指针 p
被赋值为整数变量 'value
' 的“地址”。指针具有“l 值”和“r 值”。在这种情况下,p
的 l 值为 'value
' 的地址,r 值为 15
。这允许我们将 p
传递给一个函数,在该函数(内部)中,我们可以“解引用”r
值,例如:
int copy = *p;
这条语句读作“将 p 的 r 值赋给 copy”。想象一下,您有一个函数,它接受一个 int
指针作为参数,就像这个:
void foo(int *pointer_to_value)
{
*pointer_to_value = 5;
}
在 foo
函数内部,我们可以通过解引用指针来分配一个新的 r 值。会发生什么?好了,现在原始变量“value
”变成了 5
。
好的,那么为什么我们在 C# 中需要“ref
”参数呢?嗯,“ref
”关键字用于按引用(类似指针)传递参数,而不是按值传递。为了说明这一点,我们可以将 foo
重写为:
void foo(ref int pointer_to_value)
{
pointer_to_value = 5;
}
……而我们可以像这样简单地(通过引用)传递“value
”变量:
foo(ref value);
现在为了强调本文的重点,让我们来看一下我在面试开发人员职位候选人时喜欢使用的代码片段。
Using the Code
这里有三个代码片段
// First-Scenario: Reference type (class) ===================================================
namespace ValueVsRefType
{
class Example
{
internal class foo { internal int x { get; set; } internal int y { get; set; } }
static void Main(string[] args)
{
foo value = new foo();
Console.WriteLine("Before calling ChangeFoo(): foo.x = " +
value.x + " and foo.y = " + value.y);
ChangeFoo(value);
Console.WriteLine("After calling ChangeFoo(): foo.x = " +
value.x + " and foo.y = " + value.y);
}
static void ChangeFoo(foo parameter)
{
parameter.x = 10;
parameter.y = 20;
}
}
}
// Second-Scenario Value type (struct) ======================================================
namespace ValueVsRefType
{
class Example
{
internal struct foo { internal int x { get; set; } internal int y { get; set; } }
static void Main(string[] args)
{
foo value = new foo();
Console.WriteLine("Before calling ChangeFoo(): foo.x = " +
value.x + " and foo.y = " + value.y);
ChangeFoo(value);
Console.WriteLine("After calling ChangeFoo(): foo.x = " +
value.x + " and foo.y = " + value.y);
}
static void ChangeFoo(foo parameter)
{
parameter.x = 10;
parameter.y = 20;
}
}
}
// Third-Scenario: Value type (struct) by reference =========================================
namespace ValueVsRefType
{
class Example
{
internal struct foo { internal int x { get; set; } internal int y { get; set; } }
static void Main(string[] args)
{
foo value = new foo();
Console.WriteLine("Before calling ChangeFoo(): foo.x = " +
value.x + " and foo.y = " + value.y);
ChangeFoo(ref value);
Console.WriteLine("After calling ChangeFoo(): foo.x = " +
value.x + " and foo.y = " + value.y);
}
static void ChangeFoo(ref foo parameter)
{
parameter.x = 10;
parameter.y = 20;
}
}
}
在上面的代码片段中,我们有三个几乎相同的代码。唯一的区别是:
第一种情况:结构“foo
”是一个类(引用类型)
第二种情况:结构“foo
”是一个结构(值类型)
第三种情况:“foo
”仍然是一个 struct
,但它通过引用传递(使用“ref
”关键字)
我希望您从本文中获得的是:传递引用类型(类)就像传递指针,而传递值类型(struct
)就像传递值的副本(如果您愿意的话)——在前一种情况下,我们更改了引用类型指向的内容,而在后一种情况下——我们更改了相同数据的私有副本。
使用“ref
”关键字传递引用类型当然会出问题。感谢 Astakhov Andrey 的宝贵见解——注意他写的小代码片段:
// First-Scenario: Reference type (class) ===============================================================
namespace ValueVsRefType2
{
class Program
{
class Foo
{
public int x { get; set; }
public int y { get; set; }
}
static void Main(string[] args)
{
var victim = new Foo();
Operate(victim);
Console.WriteLine(victim.x);
Operate(ref victim);
try
{
Console.WriteLine(victim.x);
}
catch(NullReferenceException)
{
Console.WriteLine("attempt to use null Foo!");
}
}
static void Operate(Foo val)
{
val = null;
}
static void Operate(ref Foo val)
{
val = null;
}
}
}
我的建议:仅在传递值类型参数时使用“ref
”关键字!
关注点
这一切都很有趣,但您是否曾想过其底层的机制是什么?嗯,这一切都与堆栈(stack)的机制有关。如果您使用过(System.Collections
)Stack
类,您就学会了“push”值(保存操作)和“pop”值(恢复操作)。在编译后的代码中,当调用一个函数时,我们首先将所有参数“push”到堆栈上。稍后,在函数内部,我们引用程序堆栈中的值——而不是堆(heap)中的值。为了说明这一点,根据第一种情况(如上),当我们 push “foo
实例”(value
)时,我们只是 push 一个指针,也就是“value
”的地址。这非常高效,因为,取决于机器的体系结构,只有几个字节(更准确地说是 WORD
或 DWORD
)被 push 到堆栈上。
然而,考虑第二种情况——“value
”类型。在这段代码中,我们导致 foo.x
和 foo.y
的实际值被 push 到堆栈上。想象一下,如果 foo
有大量的属性——我们需要 push 很多值到堆栈上。还可以想象递归的情况——即我们多次调用同一个函数。我们有可能将大量的值 push 到堆栈上。
在第三种情况(通过引用传递值类型)下,它的工作方式与第一种情况相同:我们只传递“value
”的地址。
因此,值类型被传递,并且编译器在被调用函数的范围内在程序堆栈上创建数据的私有本地副本。所以,下次当您有一个类实例(例如 foo
)并将其传递给函数时,无需使用“ref
”关键字——无论是在调用本身中,还是在你调用的函数签名中。因此,“ref
”关键字用于按引用而不是按值传递值类型参数。
C# 中的值类型:所有数字数据类型、布尔型、字符型、日期型、结构体和枚举
C# 中的引用类型:字符串、数组、类和委托
历史
这是本文的初稿。