我们提供安全,免费的手游软件下载!
C#.Net的BCL提供了丰富的类型,最基础的是值类型、引用类型,而他们的共同(隐私)祖先是
System.Object
(万物之源),所以任何类型都可以转换为Object。
C#.NET
类型结构总结如下图,
Object
是万物之源。最常用的就是值类型、引用类型,指针是一个特殊的值类型,泛型必须指定确定的类型参数后才是一个正式的类型。
?值类型 | Type | 说明 | 示例/备注 |
---|---|---|---|
byte | System.Byte | 8 位(1字节) 无符号整数 ,0到255, | |
sbyte | System.SByte | 8 位(1字节) 有符号整数 ,-128 到 127 | 不符合 CLS |
int | System.Int32 |
32位(4字节)
有符号整型
,大概
-21
亿
-21亿
|
|
uint | System.UInt32 | 32位(4字节) 无符号整型 , 0 到 42亿 | 不符合 CLS |
short | System.Int16 | 16 位(2字节) 有符号短整数 ,-32768 到 32767, | |
ushort | System.UInt16 | 16 位(2字节) 无符号 短整数 , 0 到 65535 | 不符合 CLS |
long | System.Int64 | 64位(8字节) 有符号长整形 ,19位数 | |
ulong | System.UInt64 | 64位(8字节) 无符号长整形 ,20位数 | 不符合 CLS |
Int128 | Int128 | 128 位有符号整数,.Net7支持 | |
BigInteger | BigInteger | 表示任意大小的(有符号)整数,不可变值 | |
整数表示 | - |
十六进制:
0x
/
0X
前缀;二进制:
0b
/
0B
前缀
|
var
默认为
int
|
float | System.Single | 32位(4字节) 单精度 浮点数,最多6-7 位小数,-+3.402823E38 |
后缀
f
/
F
|
double | System.Double | 64位(8字节) 双精度 浮点数,最多15-16 位小数,-+1.7*E308 |
后缀
d
/
D
|
decimal | System.Decimal | 128位(16字节) 高精度 浮点数,28位小数,-+7.9E28 |
后缀
m
/
M
|
bool | System.Boolean |
8位(1字节)布尔型,只有两个值:
true
、
false
|
|
char | System.Char | 16 位(2字节) 单个字符 ,0 到 65,535的码值,使用 UTF-16 编码 | |
enum | System.Enum | 支持任意整形的(常量)枚举,可以看做是一组整形的常量值集合 | |
Complex | Complex | 表示一个复数,实部和虚部都为double | |
Guid | Guid | 全局唯一标识符(16字节),一出生就是全球唯一的值 |
Guid.NewGuid()
|
struct | 结构体 | 是一种用户自定义的值类型,常用于定义一些简单(轻量)的数据结构 | |
DateTime | DateTime | 时间日期,详见下一章节 |
Struct
外都是不可变的。
?引用类型 | Type | 说明 | 示例/备注 |
---|---|---|---|
object | System.Object | .NET 类的顶层基类,万物之源,包括所有值类型、引用类型 | 万物之源 |
string | System.String | 字符串,引用类型,使用上有点像值类型 | 恒定性、驻留性 |
dynamic | System.Dynamic | 动态类型,编译时不检查,可随意编码,只在运行时检查 |
dynamic = 12
|
Interface | - | 严格来说并不是“类型”,只是一组契约,可作为引用申明变量 | 接口契约 |
Class | - |
定义一个引用类型,隐式继承自
object
|
定义类 |
Delegate | System.Delegate | 委托类型,详见后文《 解密委托与事件 》 | |
IEnumerable | - | 可枚举集合接口,几乎所有集合类型都实现了该接口 | |
T[] | Array | 数组,同上枚举接口,更多参考《 .Net中的集合 》 | |
匿名类型 |
new{P=v,V=1}
|
动态申明一个临时匿名类型实例, | 编译器会创建类 |
元祖 | Tuple |
内置的一组包含若干属性的泛型类,常用
(ValueTuple)
编译器支持
|
|
recored | record | 记录类型,支持class(默认)、struct,其实就是简化版的类型申明 | 编译器创建完整类型 |
System.Object
是所有类型的根,任何类都是显式或隐式的继承于
System.Object
,包括值类型,所以任何类型都可以转换为
Object
!
成员 | 描述 |
---|---|
string? ToString () | 返回对象的字符串,默认返回对象类型名称,按需重写。 |
bool Equals (object? obj) | 比较是否与当前(this)相同,引用类型比较引用地址,值类型比较值&类型,可重写! |
int GetHashCode () | 获取当前对象的哈希码,用于哈希集合Dictionary、Hashtable中快速检查相等性。但不可用于相等判断,如果重写了Equals,应同时重写GetHashCode,确保两者一致。 |
Type GetType () |
当前实例的准确运行时类型。如果是类型,在可用
typeof(T)
|
protected ~Object (); | Object.Finalize 析构函数,GC调用释放资源,按需实现。一般配合Dispose( GC.SuppressFinalize ) |
protected object MemberwiseClone () | 浅拷贝,创建新对象>赋值非静态字段,引用类型字段就是赋值引用地址了。 |
static bool Equals(Object, Object) |
比较相同,内部会调用实例的
Equals(Object)
方法
|
static bool ReferenceEquals (o1,o2) |
比较两个
引用对象
是否同一实例,⁉️注意如果用于值类型会被装箱从而始终
false
。
|
下面代码为
.Net
中的
System.Object 源码
:
public partial class Object
{
public Object(){ }
public virtual string? ToString()
{
return GetType().ToString();
}
protected internal unsafe object MemberwiseClone(); //对象浅拷贝
public virtual bool Equals(object? obj)
{
return this == obj;
}
public static bool Equals(object? objA, object? objB)
{
return objA == objB || (objA != null && objB != null && objA.Equals(objB));
}
public static bool ReferenceEquals(object? objA, object? objB)
{
return objA == objB;
}
public virtual int GetHashCode()
{
return RuntimeHelpers.GetHashCode(this);
}
~Object(){} //终结器
}
值类型和引用类型是C#中最重要、最常用的两种数据类型,两者是有很多区别的,而且常常和性能有很大关系,因此这是C#开发者必须掌握的基础知识。
区别 | 值类型 ValueType | 引用类型 ReferenceType |
---|---|---|
⭐存储位置 | 栈(Stack),也可以称为线程栈 | 堆(GC Heap),由GC管理 |
⭐存储内容 | 值 | 对象在堆上,引用变量在栈上,栈存储的的是堆上对象的内存地址 |
⭐传递方式 | 值传递,参数传递时传递的是 值拷贝 ,各回各家 | 传递的是 引用(地址) ,因此是同一个对象,分身代理 |
装箱、拆箱 |
转换为
object
、接口时会装箱、拆箱
|
无 |
默认值
default
|
数字、枚举默认0,bool默认false |
默认
null
|
占用内存大小 |
就值本身的长度,如
int
为4个字节
|
值本身+额外空间(引用对象的标配:TypeHandle、同步块) |
继承的对象 | 隐式继承自System.ValueType | 默认继承自Object |
接口、继承 | 不支持继承其他值类型,可继承接口 | 支持继承类、接口 |
怎么判断 |
Type.IsValueType
o.GetType().IsValueType
|
IsValueType ==false |
生命周期 | 作用域结束就释放,或方法结束就释放了 | 由GC管理,当对象没被使用了,GC检查后标记清除 |
性能 | 栈内存性能很高 | 低,需要GC分配内存、GC释放 |
default
可以表示任意类型的默认值,编译时会被赋值。
struct
的默认值为每个字段设置默认值。
int x = 100;
int y = x; //值拷贝传递
string name = "sam";
int[] arr = new int[] { 1, 2, 3 };
var list = arr; //引用地址传递,实际指向同一个引用对象,分身幻象
? 两者核心区别就是存储的方式不同,理解这一点非常重要,在变量(字段)赋值、方法参数传递上都是如此。
?Stack 栈 :(线程)栈,由操作系统管理, 存放值类型、引用类型变量(就是引用对象在托管堆上的地址) 。栈是基于线程的,也就是说一个线程会包含一个线程栈,线程栈中的值类型在对象作用域结束后会被清理,效率很高。
?托管堆(GC Heap) :进程初始化后在进程地址空间上划分的内存空间,存储.NET运行过程中的对象, 所有的引用类型都分配在托管堆上 ,托管堆上分配的对象是由GC来管理和释放的。托管堆是基于进程的,当然托管堆内部还有其他更为复杂的结构。
关于更多堆栈内存信息,查看后文《C#的内存管理艺术》
?值类型可使用
out
、ref
关键字,像引用类型一样传递参数地址。两者对于编译器是一样的,都是取地址,唯一区别就是ref
参数需要在外面初始化,out
参数在方法内部初始化。
因为值类型、引用类型的基类都是Object,因此值类型、引用类型是可以相互转换的,但这个转换是有很高成本的,这个过程就是装箱、拆箱。
int x = 100; //一个普通的值类型变量
object obj =x; //装箱到obj
int y = (int)obj;//拆箱到y
可视化分析一下这个过程:
?装箱 :值类型转换为引用对象,一般是转换为System.Object类型,或接口类型。所以“箱子”就是Object引用对象,装箱的过程:
obj
。
?拆箱 :引用类型转换为值类型,注意,这里的引用类型只能是被装箱的引用类型对象。
null
,类型是否和待拆箱的类型一致,检测失败则抛出异常
InvalidCastException
。
上面三行装箱、拆箱代码的IL代码:装箱
box
、拆箱
unbox
是两个专门的指令。
由上可知,装箱会在GC堆上创建一个“箱子”(Object对象)来装载值,这是装箱造成极大的性能损失的根本原因,拆箱则把值搬回到栈内存上。
?在日常开发中,很容易发生隐式装箱,所以要特别注意,尽量用泛型。如ArrayList、Hashtable 都是面向Object的集合,应该用
List
、Dictionary
代替。
ArrayList arr = new ArrayList();
arr.Add(1); //装箱
arr.Add(true); //装箱
Hashtable ht = new Hashtable();
ht.Add(1,1.2f); //装箱了两次
对比测试装箱、拆箱的性能影响:
private T Add(T arg1,T arg2) where T:INumber
{
return arg1+arg2;
}
private int AddWithObject(object x, object y)
{
return (int)x + (int)y;
}
测试结果比较明显,装箱的方法在执行效率、内存消耗上都要差很多。
可空类型可用于值类型、引用类型,他们使用语法类似,不过他们是完全不同的两种东西。值类型的可空
?
是一个泛型
Nullable
类型,而引用类型的
?
只是一个用于编译器检查的语法。
int? n = null;
string? str = "sam";
可空值类型、引用类型都支持
null
操作符:
- Null 条件运算符 ,
str?.Length
- Null 合并赋值,
n??=1
。
Nullable
对于值类型,可空值类型表示值类型对象可以为
null
值,可空值类型
T?
的本质其实是
Nullable
,他是一个值类型(结构体)。
int x0 = default; //默认值为0
int? x1 = default; //默认值值为null
int? x2 = null;
Nullable x3 = null; //同上
if (x3.HasValue)
{
Console.WriteLine(x3.Value);
}
int a = x0 + (x3.HasValue? x3.Value : 10);
int b = x0 + x3 ?? 10; //效果同上
Type?
,示例:
int? n;
,类型后跟一个问号"
?
",和引用类型的可空语法一样。
HasValue
判断是否有值,
Value
获取值,如果
HasValue
为
false
时获取 Value 值会抛出异常。
T
可隐式转换为
T?
,反之则需要显示转换。
下面为
Nullable
的源码,是一个简单的结构体,
T
被约束为结构体(值类型)。
public struct Nullable where T : struct
{
//判断是否有值
public readonly bool HasValue { get; }
//获取值
public readonly T Value { get; }
public readonly T GetValueOrDefault()
public readonly T GetValueOrDefault(T defaultValue)
}
T?
引用类型本身的默认值就是
null
,为了避免一些场景下不必要的
NullReferenceException
,就有了
可空的引用类型
T?
,其核心目的就是为提高代码的健壮性。可空的引用类型并不是一个“
新的类型
”,而是一个编译指令,告诉编译器这个引用类型变量可能是null,使用时需检查,未初始化也没检查
null
就使用,编译器会产生编译告警,由此来提前发现潜在Bug,提高代码健壮性。如果没加
?
,则该表示引用对象不会为
null
。
要开启可空引用类型需要配置启用才行:
enable
,值
disable
表示不启用。
#nullable enable
,只对当前代码文件有效。
public int GetLength(string? firstName, string lastName)
{
var len = firstName.Length; //编译器会警告 firstName 可能为null
if (firstName != null) //加上null判断就好了
len += firstName.Length;
len = firstName!.Length; //加上!,则忽略检查
len += lastName.Length; //lastName不会为null,没有告警
return len;
}
对于上面的示例代码,编译器会认为
firstName
可能会为
null
,在使用前必须初始化,或者检查是否为
null
。而
lastName
不会为
null
,可以直接使用。
? 消除可空引用类型的编译告警的方法是结尾加
!
(null 包容运算符),告诉编译器这个对象肯定不会为null
,别再告警了!
在命名空间“ System.Diagnostics.CodeAnalysis ”下还有一些特性,用来辅助代码的静态检查和编译器检查。
[NotNull] //标记返回值不会为null
private string? FullName => "sam";
//当方法返回false时参数value不会为null。该方法就是string.IsNullOrEmpty的源码
public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
{
if ((object)value != null)
{
return value.Length == 0;
}
return true;
}
数据类型之间是可以相互转换的,由一个数据类型转换为另一数据类型,常见的转换方式:
转换方式 | 说明 | 备注/示例 |
---|---|---|
隐式转换 | 转换是自动的,一般是兼容的数值类型之间, 编译时检查 |
int n =100; float f = n;
|
强制显示转换 |
使用
强制
转换操作符转换,
(Type)value
|
值类型编译时检查,引用类型运行时检查, 失败抛出异常! |
装箱、拆箱 |
值类型转换为引用类型
object
,反之为拆箱
|
装箱是隐式的,拆箱需显示转换 |
as
显示转换
|
只用于引用类型转换:
value as Type
|
运行时检查,失败返回
null
|
is
类型检查
|
检查一个值是否为指定(兼容)类型,如果是则转换 |
if(obj is float f) {}
|
类型方法
Parse
|
内置值类型基本都提供Parse、TryParse方法 |
int.Parse("123")
|
Convert | 静态类,提供了大量的静态方法来转换内置数据类型 |
Convert.ToInt16(false)
|
BitConverter | 静态类,各种内置类型和字节之间的转换方法 |
BitConverter.GetBytes(0xff)
|
XmlConvert | 静态类,提供了各种内置类型和string之前的转换方法 |
XmlConvert.ToInt32("1221")
|
TypeConverter | System.ComponentModel 空间下提供的大量类型转换器 |
var cc = TypeDescriptor.GetConverter(typeof(Color))
|
dynamic | 动态类型并不算是类型转换,作为一种特殊方式,运行时检查 |
dynamic d = Foo; d.Print();
|
?注意 :
Object
,注意值类型转换
Object
会装箱。
float
转
double
,
int
转浮点数。反之则需要强制转换,可能会损失精度,或溢出。
is
语句可用于模式匹配,实现灵活的类型、数据检查,详细参考《
C#中的模式匹配汇总
》。
int a = (int)'a'; // 强制类型转换
Console.WriteLine(a); // 97
float f = a; // 隐式类型转换
Console.WriteLine(f);
object obj = f; //装箱,值类型隐式转换为引用类型
float f2 = (float)obj; //拆箱
if(obj is float f3) //is检查是否为float类型,如果是则转换值到变量f3
{
Console.WriteLine(f3);
}
string str = obj as string; //as 转换失败,obj是float装箱
Console.WriteLine(str); //null
转换需求 | 转换方法 | 示例 |
---|---|---|
解析十进制数字 | Parse、TryParse |
int.Parse("1234")
,
double.Parse("123.04")
|
解析2/8/16进制数 | Convert.To() |
Convert.ToInt32("F",16)
//15
|
16进制格式化 | ToString("X") |
1234.ToString("X6")
//0004D2
|
无损数值转换 | 隐式转换 |
int n = 100; double d = n;
|
截断数值转换 | 显示转换 T v2 = (T)v1 |
double d=12.56d; int n = (int)d;
//n = 12,直接截断,不会四舍五入
|
四舍五入转换 | Convert.To()、Math.Round(d) |
int n = Convert.ToInt32(d);
//n = 13,四舍五入转换
int n = Math.Round(d)
//n = 13,可指定小数位数
|
? 值类型比较的是值(结构体会比较其所有字段值),引用类型是比较的引用地址!
比较操作 | 说明 |
---|---|
==、!= | 相等运算符,其本质是调用静态方法(运算符重载方法) |
a.Equals(b) |
实例的虚方法(可被重写),运行时根据实际类型调用。
? ①、
a
不能为
null
,否则就NullReferenceException了。
? ②、引用类型默认比较引用地址,值类型会递归调用每一个字段的Equals方法。 ? ③、装箱的值类型会比较箱子内的值。 |
Object.Equals(a,b) |
Object静态方法,
null
判断+
a.Equals(b)
,参数是Object,值类型会装箱。
|
Object.ReferenceEquals(a,b) | Object静态方法,只比较引用地址 |
IEquatable
|
相等接口方法
bool Equals(T? other)
|
IComparable
|
大小比较
int CompareTo(T? other)
,返回一个
int
值:
a.CompareTo(b)
// a>b 返回
1
,a==b 返回
0
, a
-1
|
>、< | 大小比较运算符,结果应该和上面 IComparable 保持一致 |
IEqualityComparer
|
扩展的相等比较接口,非泛型版本
IEqualityComparer
|
StringComparer |
提供了用于字符串的多种类型的比较器:
StringComparer.Ordinal.Compare("h","H")
|
public interface IComparable
{
int CompareTo(T? other);
}
public interface IEquatable
{
bool Equals(T? other);
}
public interface IEqualityComparer
{
bool Equals(T? x, T? y);
int GetHashCode([DisallowNull] T obj);
}
// System.Object
public static bool Equals(object? objA, object? objB)
{
if (objA == objB)
{
return true;
}
if (objA == null || objB == null)
{
return false;
}
return objA.Equals(objB);
}
Equals()
方法必须自相等,即
x.Equals(x)
必为
true
。
Equals()
方法等效于
==
,只有
double.NaN
例外,
double.NaN
不等于任何对象。而引用类型则不一定了,有些引用类型重写了
Equals()
方法,而没有重写
==
运算符。
Console.WriteLine(double.NaN == double.NaN); //False
Console.WriteLine(double.NaN.Equals(double.NaN));//True
Console.WriteLine(object.Equals(1,1)); //True //装箱比较值
Console.WriteLine(object.ReferenceEquals(1,1)); //False //装箱比较引用
StringBuilder sb1 = new StringBuilder("sb");
StringBuilder sb2 = new StringBuilder("sb");
Console.WriteLine(sb1 == sb2); //False
Console.WriteLine(object.Equals(sb1,sb2)); //False
Console.WriteLine(object.ReferenceEquals(sb1,sb2));//False
Console.WriteLine(sb1.Equals(sb2)); //True //与上面的 object.Equals(sb1,sb2) 不同
上面的 StringBuilder 重新实现了
Equals
方法,但不是继承覆盖,而是隐式
new
覆写实现的,因此只能在 通过 StringBuilder 引用调用时才有效。参考:
StringBuilder 源码
。
⁉️什么时候需要自定义相等比较?
⁉️如何自定义相等比较?
GetHashCode()
和
Equals()
方法。这两个一般是一起配对重写,需注意 二者的一致性。
!=
和
==
。
接口。
?
GetHashCode()
是基类 Object 的一个虚方法,该方法用于获取一个对象的 Int32 类型的散列码。该散列码只在 键值结构 (Hashtable、HashSet、Dictionary)中使用,用来表示元素的唯一“ID”,用于在哈希表中快速检索数据。
GetHashCode()
的默认实现:
so,如果重写了
Equals()
方法,则一般要重写
GetHashCode()
,让两者匹配。当然如果不遵守该规则也没问题,只是在使用哈希表时可能会出现问题(如性能严重下降)。
热门资讯