C# 是面向对象的强类型高级语言,内置用于存储不同类型数据的内置数据类型。每种数据类型包含特定的取值范围,使用这些数据类型来表示在应用程序中存储的数据。数据类型进一步又被分为:

  • 值类型 Value types
  • 引用类型 Reference types
  • 指针类型 Pointer types

基础概念#

值类型#

值类型特点:变量直接存储其值,派生于 System.ValueType。值类型又细分为简单类型、枚举类型、结构类型、可以为 null 的值类型。

简单类型:

  • 有符号的整型:sbyteshortintlong
  • 无符号的整型:byteushortuintulong
  • Unicode 字符:char
  • IEEE 二进制浮点:floatdouble
  • 高精度十进制浮点数:decimal
  • 布尔:bool

引用类型#

引用类型特点:引用类型不直接存储其值,派生于 System.Object,它存储值的引用内存地址。多个变量指向一个内存位置时,如果内存位置的数据是由一个变量改变的,其他变量会自动反映这种值的变化。

类类型描述

  • 所有类型的最终基类:object
  • Unicode 字符串:string
  • 格式为:class C {…} 的用户定义类型

接口类型、数组类型、委托类型,有关数值类型的详细信息。参考:整型类型 / 浮点类型表

变量#

编译器需要用某个初始值对变量进行初始化之后才能在操作中使用该变量。

// 语法
<datatype><variablename>=<value>;

// 示例
string name = "wang";

// 同时声明多个
string name1,name2 = "wang";

注意

  • 变量是类或结构中的字段,如果没有显式初始化,创建这些变量时,默认值就是类型默认值
  • 方法的局部变量必须在代码中显式初始化才能在语句中使用
  • 在C#中实例化一个引用对象需要使用 new 关键字把该引用指向存储在堆上的一个对象

变量作用域#

变量作用域指:可以访问该变量的代码区域

注意

  • 只要类在某个作用域内,其字段(也称为成员变量)也在该作用域内
  • 局部变量存在于表示声明该变量的块语句或方法结束的右花括号之前的作用域内
  • forwhile 或类似语句中声明的局部变量存在于该循环体内

类型推断#

使用 var 类型预先不用知道变量的类型,编译器可以根据变量的初始化值“推断”变量类型。

var name ="wang";

注意

  • 变量必须初始化,否则编译器就没有推断变量类型的依据
  • 初始化器不能为空
  • 初始化器必须放在表达式中
  • 不能把初始化器设置为一个对象,除非在初始化器中创建了一个新对象

常量#

常量指:其值在使用过程中不会发生变化的变量。在声明和初始化变量时,在变量的前面加上关键字 const,就可以把该变量指定为一个常量。

const string conntionName = "testConntion";

常量具有如下特点:

  • 常量必须在声明时初始化。指定其值后就不能再改写
  • 常量的值必须能在编译时用于计算。因此不能用从一个变量中提取的值来初始化常量。如果需要这么做,应使用只读字段 readonly
  • 常量总是静态的,但注意:不必(实际上,是不允许)在常量声明中包含修饰符

使用常量的好处:

  • 常量使程序变得更易于阅读(使用易于读取理解的名称替代了较难读取的数字或字符串)
  • 常量使程序更易于修改
  • 常量更容易避免程序出现错误,如果在声明常量的位置以外将另一个值赋给常量,编译器就会报错

只读字段#

常量的概念是包含不能修改的值的变量。但有时可能需要一些变量,其值不应改变,但在运行前其值是未知的。C#为这种情形提供了另一种类型的变量:只读字段。

readonly 关键字比 const 灵活,允许把一个字段设置为常量,但还需要执行一些计算,以确定它的初始值。其规则是:可以在构造函数中给只读字段赋值,但不能在其他地方赋值。只读字段可以是一个实例字段,而不是静态字段,类的每个实例可以有不同的值。与const 字段不同,如果要把只读字段设置为静态,就必须显式声明

// 实例只读字段
readonly double taxRate;

// 静态字段字段
static readonly double taxRate1;

BasicsTest()
{
    // 只读字段可以在声明时赋值,也可以在构造函数中赋值
    taxRate = 0.8;
}
static BasicsTest()
{
    // 静态只读字段在类的静态构造函数中赋值
    taxRate1 = 0.9;
}

常量和只读字段的区别#

  1. 常量只能在声明语句中初始化,而且必须初始化,初始化之后在任何地方都不能改变。 readonly 字段既可以在声明时初始化,也可以在构造函数中改变它的值。(如果是 实例只读字段 可以在实例构造函数中改变它的值,如果是 静态只读字段 则可以在静态构造函数中改变它的值)
  2. 常量的值必须在编译时决定,编译完成之后它的值就被替换为字面量。 readonly 字段的值可以在运行时决定,可以在不同的构造函数中设置不同的值
  3. 常量总是像静态字段,在类的外部要通过 类名.常量名 的方式访问。readonly 字段既可以是静态字段,也可以是实例字段
  4. 常量在内存中没有存储位置,而 readonly 字段在内存中有存储位置

值类型和引用类型空值#

默认情况下,引用类型在未初始化时具有空值。

  • 一个字符串变量(或引用类型数据类型的任何其他变量),但没有赋值。这种情况下它具有空值,这意味着它不指向任何其他内存位置
  • 值类型变量不能为 null ,因为它包含值而不是内存地址。所以必须在使用前为值类型变量分配值

可空类型#

C#2.0 为值类型引入了可空类型,允许为值类型变量赋值 null 或声明值类型变量而不为其赋值。

int? age = null;

类型转换#

C# 是一门强类型语言,对类型要求比较严格。但是在一定的条件下是可以相互转换的,如将 int 型数据转换成 double 型数据。C# 允许使用两种转换方式:隐式转换、显式转换。

隐式转换#

隐式转换是系统默认的,不需要加以声明就会自动执行隐式类型转换,在隐式转换过程,编译器无需对转换进行详细检查就能够安全的执行。隐式类型转换是 从低精度数值类型=>高精度数值类型

int a = 10;
double b = a;// 自动隐式类型转换

显式转换#

高精度值=>低精度 进行数据转换时,可能会丢失数据,这时候需要使用显式转换。并且要考虑到可能出现算术溢出。显式转换需要明确指出要转换的类型。显式转换可能导致错误,进行这种转换时编译器会对转换进行溢出检测,如果有溢出说明转换失败,表示源类型不是一个合法的目标类型无法进行类型转换,强制类型转换会造成数据精度丢失。

double a = 10;
int b = (int)a;// 显式将double类型转换为int

可空类型数据转换=>非可空类型或者另一个可空类型,其中可能会丢失数据,就必须使用显式类型转换。并且如果从可空类型转换为非可空类型时变量值为 null,会抛出 InvalidOperationException 异常。

int? a = null;
int b = (int)a;
// System.InvalidOperationException:“可为空的对象必须具有一个值。

通过方法进行转换#

ToString()

C#中的类型基类都继承自 Object 类,所以都可以使用 ToString() 来转换成字符串。

int a = 10;
string s = a.ToString();

Int.Parse()

string 类型参数转换为 int ,注意: string 类型参数不能为 null ,并且也只能是各种整型,不能是浮点型。

string a = "2";
string b = "2.6";
string c = null;
int a1 = int.Parse(a);//正常
int a2 = int.Parse(b);//错误:输入字符串格式错误
int a3 = int.Parse(c);//值不能为null

Int.TryParse()

该方法与 Int.Parse() 方法类似,不同点在于 Int.Parse() 方法无法转换成功时会抛出异常。而 Int.TryParse() 方法在无法进行转换时会返回 false , Int.TryParse() 方法需要一个 out 类型的参数,如果转换成功, out 参数的值就是正常转换的值,否则返回 false 。

string a = "2";
string b = "2.6";
string c = null;
int i;
bool a1 = int.TryParse(aout i);//转换成功,i=2
bool a2 = int.TryParse(b out i);//转换失败,a2=false
bool a3 = int.TryParse(c out i);//转换失败,a3=false

Convert()

string a = "2";
int a1 = Convert.ToInt32(a);
方法说明
Convert.ToInt32()转换为整型(int)
Convert.ToChar()转换为字符型(char)
Convert.ToString()转换为字符串型(string)
Convert.ToDateTime()转换为日期型(datetime)
Convert.ToDouble()转换为双精度浮点型(double)
Conert.ToSingle()转换为单精度浮点型(float)

自定义转换#

通过继承接口 IConventible 或者 TypeConventer 类,可以实现自定义转换。

使用 as 运算符转换#

as 只能用于引用类型和可为空的类型。使用 as 有很多好处,当无法进行类型转换时,会将对象赋值为 NULL ,避免类型转换时报错或是抛出异常。C# 抛出异常在进行捕获异常并进行处理是很消耗资源的,如果只是将对象赋值为 NULL 的话是几乎不消耗资源的(消耗很小的资源)。

object o = "abc";
string s = o as string; //执行第一次类型兼容性检查,并返回结果
if (s != null)
Console.WriteLine("转换成功!");
else
Console.WriteLine("转换失败!");

装箱和拆箱#

C#通过装箱和拆箱来实现值类型和引用类型的相互转换,使得任何 value-type 的值都可以转换为 object 类型的值,反之亦可。

装箱#

装箱是指:将值类型的数据隐式地转换成一个对象类型(object)的数据。执行装箱操作时不可避免的要在堆上申请内存空间,并将堆栈上的值类型数据复制到申请的堆内存空间上,这是要消耗内存和cpu资源的。在执行装箱转换时,也可以使用显式转换。

int i = 0;
object obj = i;   //装箱:值类型转换为引用类型

拆箱#

拆箱是指:将一个对象类型的数据显式地转换成一个值类型数据。拆箱过程是装箱的逆过程,是将存储在堆上的引用类型值转换为值类型并赋给值类型变量。

拆箱操作分为两步:

  1. 检查对象实例,确保它是给定值类型的一个装箱值
  2. 将该值从实例复制到值类型变量中
int i = 0;
object obj = i;    //装箱:值类型转换为引用类型
int j = (int)obj;  //拆箱:引用类型转换为值类型

注意

  • 装箱可以隐式进行,但拆箱必须显式
  • 在装箱的时候,并不需要显式类型转换。但在拆箱时需要类型转换。因为在拆箱时对象可以被转换为任何类型
  • 装什么拆什么,装箱就是要在托管堆重开辟空间,不但要装数值而且还要装类型。所以说装什么拆什么,也就是用什么值类型装箱,就要用什么值类型拆箱

运算符#

三元运算符#

if/else 的简化形式。首先判断一个条件,如果为真返回第一个值,为假返回后一个值。

int a = 3;
bool result = a > 10 ? true : false; //a>10?如果大于返回true否则返回false

可空类型和运算符#

C# 2.0 中出现了可空类型,允许值类型也可以为空 null,可空类型的实现基于 C#泛型。

注意:在程序中使用可空类型就必须考虑 null 值在各种运算符一起使用的影响,通常可空类型与一元或二元运算符一起使用时,如果一个操作数为 null 或两个操作数为 null ,结果就是 null 。

 int? a = null;
 int? c = a + 4; //c=null

空合并运算符#

空合并运算符 ?? 提供了快捷方式处理可空类型和引用类型时表示 null 可能的值。

注意: 只能针对引用类型处理,规则是:

  • 如果第一个操作数不是null,值就等于第一个操作数的值
  • 如果第一个操作数是null,值就等于第二个操作数的值
int? a = null;
int b;
b = a ?? 10;//第一个操作数是null,值为第二个操作数.10
a = 3;
b = a ?? 10;//第一个操作数不是null,值为第一个操作数.3

checked/unchecked#

如果把代码块标记为 checked , CLR 就会执行栈溢出检测,如果要禁止栈溢出,则可以把代码标记 unchecked 。

//byte类型最大取值255
byte a = 255;
checked
{
     a++;
}
//这里如果不加checed.++后输出0(不会抛异常,但会丢失数据,溢出的位会被舍弃,所以值为0),加上后会抛出栈溢出异常
Console.WriteLine(a);

is#

is  运算符可以检测对象是否与特定类型兼容,兼容表示对象是该类型或者派生自该类型。

转换规则如下:

  • 检查对象类型的兼容性,并返回结果 true/false 。
  • 不会抛出异常
  • 如果对象为 null,返回 false
object o = "abc";
if (o is string) //执行第一次类型兼容性检查
{
    string s = (string)o; //执行第二次类型兼容性检查,并转换
    Console.WriteLine("转换成功!");
}
else
{
    Console.WriteLine("转换失败!");
}

as#

转换规则如下:

  • 检查对象类型的兼容性并返回转换结果,如果不兼容则返回 null
  • 不会抛出异常
  • 如果结果判断为空,则强制执行类型转换将抛出 NullReferenceException 异常
object o = "abc";
string s = o as string; //执行第一次类型兼容性检查,并返回结果
if (s != null)
    Console.WriteLine("转换成功!");
else
    Console.WriteLine("转换失败!");

sizeof#

sizeof 运算符可以确定栈中值类型需要的长度(单位为字节)。

 Console.WriteLine(sizeof(int));//4个字节
 Console.WriteLine(sizeof(byte));//1个字节

typeof#

返回一个表示特定类型的 System.Type 对象。

Console.WriteLine(typeof(int));//System.Int32
Console.WriteLine(typeof(byte));//System.Byte

关键字#

C# 包含保留字,对编译器有特殊意义。这些保留字称为“关键字”。关键字不能用作变量,类,接口等的名称(标识符),关键字不能用作标识符(变量名,类,接口等)。但是,它们可以与前缀“@”一起使用。例如,class 是保留关键字,因此不能用作标识符,但可以使用 @class 。

🎨 关键字的更多信息,访问 MSDN

枚举#

enum 是值类型数据类型。枚举用于声明命名整数常量的列表。可以直接在命名空间,类或结构中使用 enum 关键字定义。

  • 枚举用于为每个常量指定一个名称,以便可以使用其名称引用常量整数默认情况下,枚举的第一个成员的值为 0,每个连续的枚举成员的值增加 1
  • 枚举可以包括数字数据类型的命名常量,例如 bytesbyteshortushortintuintlongulong
  • 枚举不能与字符串类型一起使用

Enum 是一个抽象类,包含用于枚举的静态帮助器方法

Enum methodDescription
Format将指定的枚举类型值转换为指定的字符串格式
GetName返回指定枚举的指定值的常量的名称
GetNames返回指定枚举的所有常量的字符串名称数组
GetValues返回指定枚举的所有常量值的数组
object Parse(type, string)将一个或多个枚举常量的名称或数值的字符串表示形式转换为等效的枚举对象
bool TryParse(string, out TEnum)将一个或多个枚举常量的名称或数值的字符串表示形式转换为等效的枚举对象,返回值表示转换是否成功
enum Color
{
    Red
    Green
    Blue
}

预处理器指令#

  • #region/#endregion 指令用于把一段代码标记为有给定名称的一个块

  • define/#undef 结合 #if/#elif/endif  实现条件编译

#define debug
using System;

namespace CSharp.Study.Test
{
    class Program
    {
        static void Main(string[] args)
        {
#if debug
            Console.WriteLine("debug");
#else
          Console.WriteLine("other");
#endif
        }

    }
}	
  • #if/#elif/#else/#endif 告知编译器是否要编译代码块
  • #warning/#error 如果编译器遇到#warning指令会向用户显示指令后的文本,之后编译继续。如果是#error 会在显示文本后,编译退出,不会生成IL代码