空值处理操作符
?
可空类型修饰符
// 允许值类型接受null值
public class NullableExample
{
int normalInt = null; // 编译错误!
int? nullableInt = null; // 正确,可以接受null
DateTime? nullableDate = null; // 正确
bool? nullableBool = null; // 正确
public void Example()
{
if (nullableInt.HasValue) // 检查是否有值
{
int value = nullableInt.Value; // 获取实际值
}
}
}
概述
当值类型使用
?
可空操作符时,它依然是值类型,只是被包装成了System.Nullable<T>
结构体,并不会变成引用类型。
内存分配
值类型:直接在栈上分配
可空值类型:在栈上分配,但有额外的标志位
引用类型:在堆上分配,栈上存储引用
性能特征
值类型:最高性能
可空值类型:略有性能开销
引用类型:需要垃圾回收
功能特性
值类型:不能为null
可空值类型:可以为null,需要检查HasValue
引用类型:默认可以为null
应用场景
某些场景下,我们需要区分"未设置"(null)和"默认值"(如int的0)的情况。
这里还是不明白为什么使用可空作符。疑惑点是值类型有默认值,在游戏开发中我们可以根据默认值做判断处理,为什么还需要这个可空操作符呢?并且还会带来性能开销,内存增加的问题。
想取默认值吗?有点意思。那如果是DateTime结构体呢?它的默认值是 0001年1月1日 00:00:00,当然你也可以if == "0001/1/1 0:00:00",我觉得可以理解成复杂的值类型使用比较频繁,简单的值类型像int直接if == 0就完事儿了。
简单示例
public class NullableTypeExample
{
int normalInt = 10; // 值类型
int? nullableInt = 10; // 还是值类型,Nullable<int>
Nullable<int> sameAsAbove = 10; // 与 int? 完全相同
public void Example()
{
// 检查类型
Console.WriteLine(normalInt.GetType()); // System.Int32
Console.WriteLine(nullableInt.GetType()); // System.Int32
Console.WriteLine(typeof(Nullable<int>)); // System.Nullable`1[System.Int32]
}
}
内存分配
public class MemoryExample
{
public void ShowMemoryAllocation()
{
int normal = 42; // 4字节
int? nullable = 42; // 8字节(4字节值 + 1字节标志位,对齐到8字节)
// 结构体示例
struct MyStruct
{
public int Value; // 4字节
}
MyStruct ms = new MyStruct(); // 4字节
MyStruct? nullableStruct = ms; // 8字节
}
}
性能比较
public class PerformanceExample
{
public void ComparePerformance()
{
// 值类型操作 - 直接在栈上操作
int a = 1;
int b = a; // 简单复制值
// 可空值类型操作 - 仍在栈上,但有额外开销
int? x = 1;
int? y = x; // 复制值和标志位
// 引用类型操作 - 在堆上分配
string str1 = "test";
string str2 = str1; // 复制引用
}
}
装箱和拆箱
public class BoxingExample
{
public void ShowBoxing()
{
// 普通值类型的装箱
int normal = 42;
object boxed1 = normal; // 装箱
// 可空值类型的装箱
int? nullable = 42;
object boxed2 = nullable; // 有值时才装箱
// null不会导致装箱
int? nullValue = null;
object boxed3 = nullValue; // 不会装箱,直接是null
}
}
方法调用对比
public class MethodCallExample
{
// 值类型参数
public void ProcessValue(int value)
{
// 直接使用值
}
// 可空值类型参数
public void ProcessNullable(int? value)
{
if (value.HasValue)
{
int actualValue = value.Value;
// 使用实际值
}
}
// 引用类型参数
public void ProcessReference(string value)
{
if (value != null)
{
// 使用引用
}
}
}
结构体示例
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
public class StructExample
{
public void Example()
{
// 普通结构体
Point p1 = new Point { X = 1, Y = 2 };
// 可空结构体
Point? p2 = new Point { X = 1, Y = 2 };
Point? p3 = null; // 可以赋值null
// 访问成员
if (p2.HasValue)
{
int x = p2.Value.X; // 必须先检查HasValue
}
// 使用空条件操作符
int? y = p2?.Y; // 如果p2为null,y为null
}
}
?.
空条件操作符
public class Person
{
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string City { get; set; }
}
public class NullConditionalExample
{
public void Example()
{
Person person = null;
// 传统方式
string city = null;
if (person != null && person.Address != null)
{
city = person.Address.City;
}
// 使用空条件操作符
string city2 = person?.Address?.City; // 如果person或Address为null,返回null
// 链式调用
int? length = person?.Name?.Length; // 安全地获取字符串长度
}
}
??
空合并操作符
public class NullCoalescingExample
{
public void Example()
{
string name = null;
// 传统方式
string displayName = name != null ? name : "Default";
// 使用空合并操作符
string displayName2 = name ?? "Default"; // 如果name为null,使用"Default"
// 链式使用
string result = null ?? "First" ?? "Second"; // 结果为"First"
// 与空条件操作符组合使用
Person person = null;
string city = person?.Address?.City ?? "Unknown City";
}
}
??=
空合并赋值操作符
空合并赋值操作符需要C#语言版本8.0及以上才支持
public class NullCoalescingAssignmentExample
{
private string _name;
public void Example()
{
// 传统方式
if (_name == null)
{
_name = "Default";
}
// 使用空合并赋值操作符
_name ??= "Default"; // 如果_name为null,则赋值为"Default"
// 示例2
List<string> list = null;
(list ??= new List<string>()).Add("item"); // 如果list为null,创建新列表
}
}