.NET Book Zero 读书笔记(一)(从C++的使用者角度学习C#)

一. 什么是.NET

.NET
.NET实际上是一堆动态链接库的集合,是一个大型的类的库,里面的东西可以用来做Web应用客户端应用.NET库并不是C#语言的专属,实际上任何语言都能使用.NET,但使用.NET库需要满足以下两个标准:

  • 使用.NET库的语言必须满足对应的语言要求
  • 使用.NET库的系统适用于第一条提出的编程语言,系统需要定义基本的数据成员(比如int,float,string)

上述第一条称为.NET Common Language Specification,简称CLS,第二条称为.NET Common Type System,简称CTS

CTS和CLS实际上并不是C#语言的专属,很多语言都满足这种需求,所以CLS和CTS也是Common Language Infrastructure(通用语言基础,简称CLI)的一部分。

C#语言与C++语言不同,C++语言经过编译后可以直接得到机器语言,但是像C#、JAVA这种语言与C++的编译机制不一样,.NET工程在编译后,不会生成机器语言,而是会生成一种类似机器语言的汇编语言,叫做Microsoft Intermediate Language(MSIL),简称IL(中间语言),这种中间语言还有一种更广泛的叫法,叫Common Intermediate Language(简称CIL)。


二. 机器语言、汇编语言与C++和C#之间的关系

首先有以下概念:

  • Machine Code:机器语言
  • Native Code
  • Unmanaged Code
  • Managed Code
  • Assembly Code:汇编码
  • Assembly Language

Machine Code
机器语言是最简单的,就是简单的二进制语言,没有人能读懂,直接由计算机硬件执行。

Navtive Code
这个概念有点混淆,有时候指的是Machine Code,有时候指的是Unmanaged Code。

Unmanaged Code
用C或C++语言写的代码,叫做Unmanaged Code,会被编译器直接Compile成Machine Code.

Managed Code
由C#,、VB.NET、 Java等语言写的代码,叫做Managed Code,这种语言会在虚拟的环境上(虚拟机)上进行编译(比如说C#对应的.NET、JAVA对应的.JAVA VM)
Perl, Python, PHP 和Ruby这些动态类型的语言严格意义上也是Managed Code,但是通常Managed Code还是指的是C#和JAVA这种大型商业语言。

Unmanaged Code 与 Managed Code的区别
Managed Code会自动执行GC(Garbage Collector),而且是通过引用来获取物体
(managed code “manages” the resources (mostly the memory allocation) for you by employing garbage collection and by keeping references to objects opaque.)
Unmanaged code 需要手动分配和释放内存,忘记释放内存会造成Memory Leak,过早释放内存会造成segmentation faults 。Unmanaged Code没有在运行时对空指针和数组越界的检查机制,所以这也是为什么我们用C#能轻易的知道哪里数组越界,但是在C++就很难知道具体的报错情况。

Assembly Code
汇编码(Assembly Code)是方便人们阅读的一种二进制代码,叫做Assembly Code,这种语言叫汇编语言,举个例子,C++中,对一个.cpp文件可以编译得到对应的.obj文件,.obj文件是Machine Code,是纯粹的机器语言,但是为了方便阅读,我们可以通过VS项目设置,在编译时生成相应的.asm文件,如下图所示:
在这里插入图片描述

通过使用Assembler(汇编器)这个程序,可以将Assembly Code转换成真正的Binary Code,这个转换过程与编译过程不同,因为这个过程的转换相当于从1-转换为-1,都是二进制代码的转换,本质上转换的内容并不多。之所以叫Assembly Code,是因为这些代码都会被Assembler转换成最后真正的二进制代码

通过Assembler将Assembly Code转换后二进制代码,有多种不同的类型,对于C/C++这种Unmanaged Code,得到的二进制代码就是真正的Machine Code,真正的硬件可以直接进行读取,而对于C#或JAVA这种Managed Code,得到的是一种特殊的二进制代码,这些二进制代码虚拟机上运行的,比如C#,汇编码会被转换为CIL(Common Intermediate Language)代码,在JAVA叫做JAVA byte-code。

CIL是汇编语言吗?
学完上述概念我还是有这个疑问,不过有一篇很好的微博解释了这个问题:
https://www.cnblogs.com/eaglet/archive/2009/06/02/1494290.html
这个问题略有争议,首先,汇编语言是可读的,而CIL也是可读的,CIL在定义上讲是汇编语言,但在C++里面我们所说的汇编语言转换后的机器语言,是直接运行与真正的硬件上的,在C#和JAVA这种的CIL转换成二进制语言后,是运行与虚拟机上的,有人觉得汇编语言是低级的机器语言,所以会觉得CIL略显高级,不能属于汇编语言,但总的来说,CIL也是汇编语言。


三. .NET Runtime

运行.NET程序,需要两个环境前提,一是安装.NET runtime,二是安装.NET Framework Sofware Development Kit(SDK)

在命令行可以把C#的编译器加入到环境变量之中,具体参考链接https://blog.csdn.net/yual365/article/details/87283945。
可以用notepad命令写一个.cs文件,代码很简单,如下所示,用csc(csc是C#语言的编译器,一般在.NET FRAMEWORK文件夹下)运行后,可以得到一个.exe文件

class FirstProgram
{
    
    
    public static void Main()
    {
    
     
         System.Console.WriteLine("Hello World!");
    }
}

对于C#生成的.NET的.exe,SDK提供了一个工具,叫做ildasm.exe(IL Disassembler),该工具也叫反汇编工具,打开该软件,可以利用该软件将之前生产.exe文件转化为可阅读的CIL语言,如下所示:
在这里插入图片描述
其中,ldstr是load string的缩写,call来调用函数,利用.NET的CLR(Common Language Runtime)可以将上述CIL转化为真正的机器语言


四. Assembly

Assembly本意是集合的意思,Assembly可以包含多个文件,在C#中代表着已经编译好的代码库,也就是由CIL组成的代码,会再被CLR转换为真正的机器语言,Assembly分为两种:

  • process assemblies (EXE)
  • library assemblies (DLL)

五. MSBuild

.csproj
在Visual Studio中使用.NET生成的项目,会有一个格式为.csproj的文件,这个文件具有以下特点:

  • 是XML文件
  • 包含了对cs文件的引用
  • 包含了VS维护和编译项目的信息

MSBuild is the build platform that enables all build activity in the Visual Studio world.
主要为:

  1. 所有的.csproj文件都是msbuild文件(The .csproj files (every C# project) are msbuild files)
  2. 按F5时,相当于调用msbuild.exe来进行编译(When you hit F5, you basically (oversimplifying) call msbuild.exe and passing in your .csproj file.),MSBuild软件会调用Csc和其他的一些工具。

比如说在.csproj所在的文件夹对应目录的命令行,输入msbuild myprogram.csproj,会自动编译该项目,并在对应路径生成.exe文件。

如果需要用命令行来buildC#工程,可以使用MSBuild程序来进行。


六. C#中的Field和Property

在C++中,为了安全,对于一个类的数据成员,往往是将其设置为private,然后使用对于的Get和Set功能的API去调用和写入值,在C#中,可以通过Field和Property来实现对应的功能,用一句最简单的话就是,Properties expose fields,Property是field的接口,如下述代码所示:

public class MyClass
{
    
    
    // this is a field.  It is private to your class and stores the actual data.
    private string _myField;

    // this is a property. When accessed it uses the underlying field,
    // but only exposes the contract, which will not be affected by the underlying field
    public string MyProperty
    {
    
    
        get
        {
    
    
            return _myField;
        }
        set
        {
    
    
            _myField = value;
        }
    }

    // This is an AutoProperty (C# 3.0 and higher) - which is a shorthand syntax
    // used to generate a private field for you
    public int AnotherProperty{
    
    get;set;} 
}

注意:field成员一般(或者说总是)被声明为private,C#3.0以后支持只写Property而不用在类里再写一个private的field,会自动生成对应的field

更多详情可以参考StackOverflow


七. C++和C#的编译方式

对于库文件,或者是lib或者是dll,C++ 中先是把自己的cpp文件编译为.obj文件,(编译阶段只需要用其头文件知道函数使用方式就好),然后用Linker去链接他们,而C#就没有这么多过程,之前提到了,C#就是一堆dll的集合,C#的编译器,或者说CLR自带这些dll,所以省去了Link这一操作,相应的函数都是CLR自带的。C#中,大多数类与结构体的实现在mscorlib.dll中.

对于第三方库文件,需要在项目中给CLR添加引用,(如果是命令行用/r),如下图所示:
在这里插入图片描述

八. C#中的using指令

代码如下所示

using System;	//这玩意儿叫 directive
using System.name1;

using System的含义是告诉编译器,如果找不到特定的函数,比如说Console.WriteLine(),就尝试把System.加到函数前面再去寻找这个函数。注意,using System这一句话跟头文件没有关系,因为C#没有头文件的说法,也跟库文件没有关系,这一行指令与引用无关,using System只是一种单纯的文本简化的写法,如果不要这句话,换成System.Console也可以,只是告诉我们System是一个命名空间,如果使用的函数找不到,就去这里找。

using 指令可以帮助实现文本简化,如果有两个namespaceA和B,二者都定义了print函数,在一个cs文件需要用到两个print函数,这个时候,这么写:

======= 这种写法是错误的,因为并没有说明到底调用A.print还是B.print ========
using A;
using B 
void Func()
{
    
    
	print();
}
====== 分别调用======

void Func()
{
    
    
	A.print();
}

如果A、B名字太长,可能会很麻烦,比如A是KareninaSoftware.HandyDandyLibrary,B叫BovaryEnterprises.VeryUsefulLibrary,每次这么写就太麻烦了KareninaSoftware.HandyDandyLibrary.print(),可以简化成:

using Emma = BovaryEnterprises.VeryUsefulLibrary;
using Anna = KareninaSoftware.HandyDandyLibrary;
void Func()
{
	Emma.print();
}

写DLL时,一定要用自己的namespace把它包含起来,否则别人用的时候报错了毫无办法。


九. String

C#的String和C++的String
在C++里,可以用char数组或string类来表示string,C++用数值为00的字节作为字符串结束的标识,在C#中,String是一个类,一个string字符由Char类的成员组成,也可以用小写的string,后者是System.String的别名,同理,char是System.Char的别名,int是System.Int32的别名。

二者的区别在于:
Strings are immutable and hence the characters of strMyCSharpString can‘t be altered(C#的string不允许针对特定索引位置进行改变)

C#的string的索引位置只可读,不可写,具体是这么定义的:

        public char this[int index] { get; }
        // 所以C#里,这么写会报错
        str1[0] = 'h'; // Error

这也意味着,对于一个string a,很多C++中操作string的函数是直接改变a的值,而C#中则是创建一个新的string,不改变a的值。举个例子,代码如下:

string a; // 或String a
// 把a里的字母改为大写

// 在C++中这么写
_strupr(a); // a中的字母改为大写

// 在C#中这么写
a.ToUpper(); // a不变
String b = a.ToUpper(); //b是a的字母改为大写后的结果
a = a.ToUpper(); // 或者这么写,原来的值会由于未被引用而被GC回收

string改变特定索引位置的值
可以用对应的api

string str = "abcdifg";
str[4] = 'e'; // Won't work! 
str = str.Replace('i', 'e');	// 替换元素
str = str.Remove(4, 1);	// 删除第四个索引的一个元素
str = str.Insert(4, "e");	
str = str.Remove(4, 1).Insert(4, "e");	// 同时删除和添加操作
str = str.Substring(0, 4) + "e" + str.Substring(5);	// 取子串

也可以先转成char数组,再转回去

char[] buffer = str.ToCharArray();
buffer[4] = 'e';
str = new string(buffer);

C#中的String不允许单斜杠的存在,如下图所示,在表示路径时需要用“\”
在这里插入图片描述
如果不想让string里面的内容发生改变,那就在string前面加上@符号,如下图所示:
在这里插入图片描述
如果想要在string里面显示引号,对于双引号,原本的"A"要写成"“A”", 单引号不变,如下图所示:
在这里插入图片描述
也可以借助转义符号\来输出"",如下图所示:
在这里插入图片描述

ToString方法
C#中每个实例对象都继承于Object类,所以定义了ToString方法,ToString有以下四种输入类型,可以很灵活的进行格式的转换:

string ToString()
string ToString(string format)
string ToString(IFormatProvider provider)
string ToString(string format, IFormatProvider provider)

举一些具体使用的例子:

    Console.WriteLine("Currency C3: " + Math.PI.ToString("C3"));	// 表示货币,输出¥3.142
    Console.WriteLine("Exponential E3: " + Math.PI.ToString("E3"));	// 科学记数法(Exponential format)
    Console.WriteLine("Fixed-Point F3: " + Math.PI.ToString("F3"));	// 小数点保留三位,输出3.142
    Console.WriteLine("General G3: " + Math.PI.ToString("G3"));	// 三位有效数字
    Console.WriteLine("Number N3: " + Math.PI.ToString("N3"));	// 每千位会加一个逗号
    Console.WriteLine("Percent P3: " + Math.PI.ToString("P3"));		// 百分数
    Console.WriteLine("Round-Trip R3: " + Math.PI.ToString("R3"));	//	获取所有位
    Console.WriteLine();
    Console.WriteLine("Fixed-Point F3: " + 12345678.9.ToString("F3"));
    Console.WriteLine("General G3: " + 12345678.9.ToString("G3"));
    Console.WriteLine("Number N3: " + 12345678.9.ToString("N3"));
    Console.WriteLine();
    Console.WriteLine("Decimal D3: " + 55.ToString("D3"));
    Console.WriteLine("General G3: " + 55.ToString("G3"));
    Console.WriteLine("Hexadecimal X3: " + 55.ToString("X3"));

输出结果如下:
在这里插入图片描述
ToString不只是有一个参数,其他的参数可以规定一些文本的格式,举个例子,可以规定输出不同的货币。
在这里插入图片描述
String里常见的实用操作
这种替换好像还挺常见的,差不多就是把它当宏用了:

Console.WriteLine("{0} times {1} equals {2}", A, B, A * B);
Console.WriteLine("{2} equals {0} times {1}", A, B, A * B); // 逆序也可以
Console.WriteLine("{0} times {0} equals {1}", A, A * A); // 反复出现也可以
Console.WriteLine("{1} times {2} equals {4}", C, A, B, D, A * B, E); // 省去第三个也可以
String s = String.Format("{0}", "Hello");
Console.WriteLine("{0} is a curly bracket {
   
   { }} ", "here"); // 如果想输出{ }需要写成{
   
   { }}

Console.WriteLine("Please deposit {0} dollar{1}.", dollars, dollars == 1 ? '' : 's'); // 当只有一刀时,dollar后面不加s

配合上面的{0}写法,可以使用分号加字母规定输出数字的格式:

    double A = Math.PI;
    double B = Math.PI;
    Console.WriteLine("{0:N2} times {1:N3} equals {2:N4}", A, B, A * B);
	Console.WriteLine(A.ToString("N2") + " times " + B.ToString("N3") +
 " equals " + (A * B).ToString("N4"));

结果输出如下:
在这里插入图片描述

还可以规定{}里字符串的最短长度,如果不够就用空格补齐,即使超过这个长度,也会输出字符串,不会截断:

Console.WriteLine("{0,5:N2} times {1,5:N2} equals {2,10:N4}", A, B, A * B);	// 第一个A至少占一个5个字符,A*B的输出至少占10个字符

注意,C#使用Console.WriteLine时,实际上是调用String.Format函数,再把得到的string打印出来。

另外,还有个有意思的地方,在C#里Debug的时候,下方Watch窗口里会显示类下的ToString返回的内容,如下图所示:
在这里插入图片描述


十. C#中的转义字符(Escape Character)

有这几种转义字符\t,\n,\r,\r\n\t代表加Tab,\n代表换行符,\r是从最老的打字机引入的概念,表示回到本行的开始位置,\r\n连用,表示跳到下一行,并且返回到下一行的起始位置,额外提一下\r,代表回到本行的开始位置,如下所示:

string s = "1234\r567";

最后输出的s是5674,前面的123被覆盖掉了。


十一. C#中的Constructor

C#也有构造函数,都是在调用new关键字时调用的构造函数,类与结构体都可以定义构造函数,如下所示:

int index = new int(); // 默认构造函数会把index设置为0
int index = new System.Int32(); // int是Int32的别名

十二.C#的Object类

Object类是所有.NET的类与结构体的鼻祖,都继承与它,Object里定义了ToString()的虚函数,所以所有的C#内的对象都有.ToString函数,这也是为什么,我们可以轻易的将任何类型的数据与string对象相加,因为这些类型都会隐式调用其.ToString函数,如下图所示:
在这里插入图片描述


十三. C#中的Environment类

C#为实现跨平台定义了一个类Environment,这个类有很多跨平台相关的功能:

// 跨平台保证成功换行
string NL = Environment.NewLine;
string res = "There once was a coder named Otto" + Environment.NewLine;

还有更多的内容:

    class ShowEnvironmentStuff
    {
        static void Main()
        {
            // 获取当前目录
            Console.WriteLine("My Documents is actually " +
            Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
            Console.WriteLine();
            // 获取当前设备已运行的时间
            int msec = Environment.TickCount;
            Console.WriteLine("Windows has been running for " +
            msec + " milliseconds");
            Console.WriteLine("\tor " + msec / 3600000.0 + " hours");
            Console.WriteLine();
            // 获取当前的操作系统版本和.NET版本
            Console.WriteLine("You are running " + Environment.OSVersion);
            Console.WriteLine("\tand .NET version " + Environment.Version);	// Environment.Version类型是Version而不是String,这在C#里是很常见的
            Console.WriteLine();
        }
    }

十四. C#中的static

static可以用于类或类内的成员,用于类的成员或成员函数时,表示该内容是类共用的,当用于类时,表示该类是静态的,不可以被实例化,所以该类不可以调用ToString成员,因为没有实例

        class A
        {
    
    
            private class B
            {
    
    
            }
        }

        static class C
        {
    
    
            private class B
            {
    
    
            }
        }

        static void Main()
        {
    
    
            A a = new A();	// 编译正确
            C c = new C();	// 编译错误,不可以实例化一个static类的对象
        }

Int32和Double类不仅仅是有实例化的函数,也有少量的静态函数,比如Parse函数,可以将字符串类型的数字提取出来:

  Console.WriteLine(Int32.Parse("42132"));	// 正确
  Console.WriteLine(Int32.Parse("2124f"));		// 运行时报错,字符串格式不对
  Console.WriteLine(Int32.Parse("asf"));		// 运行时报错
  
  // 为了保证不报错,可以验证字符串的格式
  int a = 1;
  Int32.TryParse("asf", out a);
  Console.WriteLine(a);	//	如果格式错误a会变成0

也可以这么写,不过比较古怪:

 int a = int.Parse("123");	// int是Int32的别名


十五. C#的基础数据类型

在C++里,int类型最低4个字节,short最低2个字节,long类型最低4个字节,如下图所示:
在这里插入图片描述
但在C#中,这些数据结构都被定死了大小,跟平台无关,C# short is always 16 bits wide, the int is 32 bits wide, and the long is 64 bits wide. 、

隐/显式转换
低精度的可以隐式转换为高精度的,但是高精度的必须显式转换为低精度的类型:

    int a = 5; // 正确
    long b = a;	// 正确
    int c = b;	// 编译错误
    int d = (int)b;	// 正确

正常在浮点数后面加f,同样,在u类型后面可以加U或u,在long类型后面加L或l,写法相当灵活:

// 以下写法都是对的
uint a1 = 54u;
uint a2 = 54U;
long b1 = 54l;
long b2 = 54L;
ulong c1 = 54UL;
ulong c2 = 54Ul;
ulong c3 = 54ul;
ulong c4 = 54uL;
ulong c5 = 54LU;
ulong c6 = 54Lu;
ulong c7 = 54uL;
ulong c8 = 54lu;

C#里无法表示8进制和2进制数
C++里用0x代表16进制数,用0表示8进制数,(在C++14中支持2进制数,用0b表示),C#里只能表示16进制数,如下所示:

// 0x或0X均可
int hex1 = 0X4AbC;
int hex2 = 0x4AbC;

byte和sbyte
byte相当于C++的unsigned char类型,sbyte相当于C++的char类型

const
C#中的const很类似于一个宏,const常量必须在初始化的时候被赋值,const常量不占任何运行时的空间,只是在编译时替换而言,const must be available at compile time

checked
使用checked可以使得当存在overflow/underflow时报错,可以这么写B = unchecked(5 * A);,也可以用括号指定一个区域,举个例子,3443243 * 3443243 = 11,855,922,357,049‬:
以下代码不会报错,但输出结果不对:
在这里插入图片描述
使用checked之后可以提示这种错误:
在这里插入图片描述
但是用checked会降低效率,其实在项目属性里也能在Build的高级选项里选择是否检查数据溢出,如果不想在检查溢出的环境里检查特定的数据,可以用uncheck,写法是一样的

C#中的符号位数据结构不符合CLS标准
CLS(Common Language Specification)里规定了使用,NET库的最低语言要求,但里面没有规定要实现带有unsigned类型的数据成员,所以C#中以下变量ushort、uint和ulong是不符合CLS标准的,如下图所示:
在这里插入图片描述
在这里插入图片描述
这会造成一个很大的影响,当写C#用于Dynamic Link Libraty(DLL)库文件时,需要避免使用带有unsigned符号的数据类型,具体有以下几种:

  • 导出public函数的参数和返回类型都不允许使用unsigned符号的数据类型
  • 不允许声明public的unsigned符号数据类型的Property和Field成员

比如说这里就有一个问题关于此问题报错的链接:https://stackoverflow.com/questions/14013920/returning-a-unsigned-long-array-from-c-dll-into-c-sharp-as-uint-why-is-a-marsh

Char
char类型占2个字节,Char类型里定义了很多函数,可以来判断是否是数字、字母等,比如IsControl, IsSeparator, IsWhiteSpace, IsPunctuation, IsSymbol, IsDigit, IsNumber, IsLetter, IsUpper, IsLower, IsLetterOrDigit, IsSurrogate:

Char.IsControl(str[index]);	// 是否为control字符
Char.IsControl(str, index);

Doube和Float
float是类Single的别名,double是类Double的别名:
写法有多种,都得要看懂
在这里插入图片描述
注意,正常低精度像高精度数据类型转换都是隐式转换,高精度向低精度类型都是显式转换,但是用于浮点数可能有点特殊,因为浮点数可以表示无穷大和出错的数据,所以浮点数转向别的一般都需要显示转换。

Decimal
Decimal为SQL Server、MySql等数据库的一种数据类型,可以在定义时划定整数部份以及小数部分的位数,以保证存储的数据==完全精确=。
C中没有这个类型,在做数据库C语言开发时,可以将此类型数据定义为double类型数据。这种类型经常用于金融、利率等方面。

decimal m = 55.23m;

Decimal类型一共128个位,也就是128/8 = 16个字节,其中1个位为符号位,96个位为数字位,还有5位用来表示小数点的位置(准确的说是28位,因为2^96 = 7.9228163e+28,最多28个数字),也可以叫做scaling factor,因为代表着10^(-scaling factor),剩余的26位是无效位

举个例子,对于12.34,其数字位是1234,1234 = 0x4D2,scaling factor为-2,代表着1234 * 10^(-2),可以看到,decimal类型的数字,原值是多少,就是多少,而不会像浮点数一样,总有精度问题,所以decimal类型适合用于金融,因为这是完全精确的数字记法

还可以在C#里这么构造一个decimal

// 前三个数是三个int,一共加起来是32 * 3 = 96位,后面是符号位和10的指数位,scale在0~28之间
// 注意输入的虽然是int,但是会被当做unsinged int处理(为什么不输入uint类型,是因为CLS中没有unsigned int这个类型)
decimal m = new decimal(low, middle, high, isNegative, scale); 

//12.34567
new decimal(1234567, 0, 0, false, 5)//最大的decimal数字,因为-1是负数,负数在计算机是用补码表示的
//1000 0000 0000 0000 0000 0000 0000 0001(32位iint的-1原码)
//取反码加一表示成补码后,正好是0xffffffff
new decimal(-1, -1, -1, false, 0); 

//1 × 10^(-28)
new decimal(1, 0, 0, false, 28);

还可以反向解析出Decimal数据:

// 解析出decimal数据
//  A[0], A[1], andA[2]) are the low, medium, and high components
int[] A = Decimal.GetBits(m);

//The fourth element contains the sign and the scaling factor. Bits 0 through 15 are 0; bits 16 through 23 contains a scaling factor between 0
//and 28; bits 24 through 30 are 0; and bit 31 is 0 for positive and 1 for negative. In other words, 
//if A[3] is negative, the decimal number is negative. The scaling factor is:
// A[3]最右边16位全是0,是无用数据
(A[3] >> 16) & 0xFF

注意几点:

  • CIL 并不包含Decimal类型
  • 不可以将check和uncheck用于Decimal类型
  • 虽然decimal类型的范围比float小,但是由于decimal的精度比float高,所以二者相互转换时,也需要显示转换

Math
Math类位于System下,有两个常数:PI和E(对数那个e = 2.7…)

有Abs函数,MAX,MIN函数,还有Sign函数(用来判断正负号的),关于Abs函数注意这一点:

 // 下面两句都会报错,OverflowException,因为int类型最小值的绝对值比最大值的绝对值要大一位
 float x1 = Math.Abs(Int32.MinValue)
 float x2 = Math.Abs(Int16.MinValue);

还有用于一些用于乘法和除法的API:

long l = Math.BigMul(i1, i1);	// 防止int乘法溢出,用long类型作为结果
long l = (long)i * i;	// 这样写跟上面一行作用是一样的
c = Math.DivRem(a, b, out d); // c = a/b, d =a%b

Math里面用于取整的函数:

Math.Floor(3.5); // returns 3
Math.Ceiling(3.5); // returns 4
Math.Floor(-3.5); // returns  -4
Math.Ceiling(-3.5); // returns -3

// Floor : round toward negative infinity
// Ceiling : rounding toward positive infinity
// (int): rounding roward zero,也叫truncation(截断)
int(1.4); // returns 1
int(-1.4); // returns -1

// Math.Round(): 取整函数
Math.Round(4.5)// returns 4
Math.Round(5.5)// returns 6(0.5的时候优先返回偶数)
Math.Round(5.5, MidpointRounding.ToEven); // returns 6 0.5的时候返回偶数,等同于上句话
Math.Round(5.5, MidpointRounding.AwayFromZero); // returns 5
Math.Round(5.285, 2); // returns 5.28
Math.Round(5.285, 2, MidpointRounding.AwayFromZero); // returns 5.29

Math中三角函数和反三角函数,统一由弧度制来表示,如下所示:

Math.Sin(Math.PI * angle / 180);

反三角函数是在对应的API前面加上字母A,如下所示:
在这里插入图片描述



十六. Expression and Statement

Expression: Something which evaluates to a value. Example: 1+2/x
Statement: A line of code which does something. Example: GOTO 100



十七. C# 中的运算符和表达式

运算符优先级
在这里插入图片描述

C#中的位运算符
C++中的布尔值之间的逻辑判断是通过&&||实现的,单个符号&代表位运算符,而C#中的&符号,不仅代表了位运算符,还可以代表逻辑运算,也就是说C++里是不能用&表示逻辑运算符的,但是C#中可以,举个例子:,举个例子:

    int x = 0x0001;	
    int y = 0x0002;
    bool a = true;
    bool b = false;
    Console.WriteLine(a && b); // 打印false
    Console.WriteLine(a & b); // 打印false
    Console.WriteLine(a ^ b); // 打印true
    Console.WriteLine(x | y); // 打印3

Switch case在C#和C++中的差异
C++中,可以这么写:

switch(a)
{
    
    
	case 3:
	 b = 7;
	 // Fall through isn’t allowed in C#
	case 4:
 	c = 3;
 	 break;
	default:
 	b = 2;
 	c = 4;
	 break;
}

但在C#中,就得用goto:

switch (a)
{
    
    
	case 3: //如果没有任何操作
	case 4:
 	b = 7;
 	c = 3;
	 break;
	default:
	 b = 2;
 	c = 4;
	 break;
}

switch (a)
{
    
    
	case 3: //使用goto来代替
	b = 7;
	goto case 4;
	case 4:
 	b = 7;
 	c = 3;
	 break;
	default:
	 b = 2;
 	c = 4;
	 break;
}

foreach in C#
the foreach statement works with collections that implement the IEnumerable interface,foreach里的元素必须实现IEnumerable接口。

关于foreach值得注意的一点是,foreach里的取得的每一个元素本身的值是只读的,但里面的值是可以改的,具体的可以举个例子:

// 下面这个foreach会报错
foreach (var item in MyObjectList)
{
    
    
    item = Value;
}
// 但是这个就不会
foreach (var item in MyObjectList)
{
    
    
    item.Value = Value;
}

具体解释见下:
foreach is a read only iterator that iterates dynamically classes that implement IEnumerable, each cycle in foreach will call the IEnumerable to get the next item, the item you have is a read only reference, you can not re-assign it, but simply calling item.Value is accessing it and assigning some value to a read/write attribute yet still the reference of item a read only reference.

跳转指令(Jump Statements)

  • return
  • goto
  • break
  • continue
  • throw

goto
前面提到了goto可以用于switch语句,还可以在C#中设置label,然后用goto跳转到那里,代码如下所示:

static void Main()
{
    
    
    for (int i = 0; i < 300; i++)
    {
    
    
        if (i == 55)
        {
    
    
            goto Label;
        }
    }
    Label: // 用冒号声明一个可以跳转的label
    return ;
}

注意,goto必须在label的同一block或者是子block里,不然goto的时候如果label不存在就尴尬了:

for (int i = 0; i < 300; i++)
{
    
    
    if (i == 55)
    {
    
    
        goto Label; // Error, No such 'Label' within the scope of goto statement
    }
    for (int j = 0; j < 300; j++)
    {
    
    
        Label:
    }
}


十八. Stack and Heap

Heap
Windows下的每一个进程都有一个Local Heap,由其内部的所有线程共享

== A reference type is stored on the stack as a reference to an area of memory allocated from the heap==
举个例子,String 类型的变量a,其引用的地址在Stack里,但是其真正的数据在Heap上。
这样做是为了提高效率,因为Heap上找数据很慢,为什么很慢,这里有两个原因:

  • Heap上分配内存时,经常进行内存整合,特别是内存不太够用的时候
  • Heap需要进行GC

Stack不适合大小会变的数据

Memory allocated from the heap is always initialized to zero
比如int[] A = new int[100],初始值都是0,至于引用类型的变量,同样也是这样,string[] strs = new string[10则下面的strs[5] == null是true

C#中Reference和Pointer的区别

  • The reference is managed, and the memory it references is known as the managed heap
  • The program can‘t manipulate or perform any arithmetic on this reference
  • most importantly, if a memory block allocated from the heap no longer has any references pointing to it, that memory block becomes eligible for garbage collection

第三条就提到了GC的本质,对于堆上的物体,如果没有任何指向它的引用,则其会被GC销毁。

GC
并不是说只有创建类这种显示的方法才会有GC,下面这种隐式的方法一样会有GC,因为实际上等式右边一直在Heap上创建类的实例:

public class TestE1 : MonoBehaviour
{
    
    
    string s;
    // Update is called once per frame
    void Update()
    {
    
    
        for (int i = 0; i < 280; i++)
        {
    
    
            s = string.Format("HHAHAHA");
        }
    }
}

public class TestE2 : MonoBehaviour
{
    
    
    string s;
    // Update is called once per frame
    void Update()
    {
    
    
        for (int i = 0; i < 280; i++)
        {
    
    
            s = string.Format("{0}/HHAHAHA", i);
        }
    }
}

有意思的是,拿Unity进行测试,TestE2的GC差不多是TestE1的三倍,我觉得应该是GC并不是立马进行的缘故,一段时间后原来的“HHAHAHA”还没被释放,又被引用了,这就省了很大一部分的GC

String A = null; // A的引用值为0,heap上没有任何反应
String B;// Stack上分配了记录引用值的空间,但需要进行初始化
String C = ""// Stack和Heap上都分配了对应的内存

举个例子,下面语句并没有用到new,但是却是在堆上进行了内存的分配

double[] D = {
    
     3.14, 2.17, 100 };
// 上面这句话其实等同于:
double[] D = new double[]{
    
     3.14, 2.17, 100 };


十九. Array

Any array is implicitly an object of type System.Array.

int[] A; // 声明一个数组,A是一个引用类型,会在Stack上预留一块存Array引用的空间,但还没初始化
A = null; // 初始化A,这个时候A = 0,Heap上还是没有分配内存
A = new int[100]; // Heap上分配了100个32位的整数数组,改A作为首地址的引用,在堆上初始化的,其初始值都是0

以下是几种常见的声明方式:

double[] D = new double[3] {
    
     3.14, 2.17, 100 };
double[] D = new double[] {
    
     3.14, 2.17, 100 };
double[] D = {
    
     3.14, 2.17, 100 };
// 但是这么写不行
double[] D;
D = {
    
     3.14, 2.17, 100 }; // Won’t work!

多维数组

int[,,] three;
three = new int[8, 5, 3];
 three.Rank // returns 3,代表数组的维度
three.GetLength(1); // returns 5
three.Length; // returns 8*5*3 = 120

string[,] senators = new string[50,2]; // That‘s 50 states and 2 senators each
int[,,] arr = new int[3, 2, 4] {
    
    {
    
    {
    
     8, 3, 4, 2}, {
    
     7, 4, 1, 2}},
 {
    
    {
    
     2, 7, 3, 6}, {
    
     5, 1, 9, 0}},
{
    
    {
    
     0, 4, 9, 7}, {
    
     3, 9, 8, 5}}};

// 也可以这么写
int[,,] arr = {
    
    {
    
    {
    
     8, 3, 4, 2}, {
    
     7, 4, 1, 2}},
 {
    
    {
    
     2, 7, 3, 6}, {
    
     5, 1, 9, 0}},
 {
    
    {
    
     0, 4, 9, 7}, {
    
     3, 9, 8, 5}}};

多维数组支持低维度的数组的大小不是统一的

string[][] jaggedArray = new string[4][];
jaggedArray[0] = new string[5];
jaggedArray[1] = new string[2];
jaggedArray[2] = new string[8];
jaggedArray[3] = new string[4];

// 也可以指么写
string[][] jaggedArray = new string[4][]
 {
    
    
 new string[] {
    
     "Jill", "Alice", "Billy", "Judy", "Sammy" },
 new string[] {
    
     "James", "Ellen" },
 new string[] {
    
     "Steve", "Sue", "Bernie", "Rich",
 "Chris", "Erika", "Michelle", "Alyssa" },
 new string[] {
    
     "Jack", "Diane", "Bobby", "Sally" }
 };

const和static readonly的区别

C#中,const是在编译时期就进行了替换,而static readonly是运行时的,二者基本差不多
A const is evaluated during compilation and the value is substituted wherever it‘s used. A static readonly field is evaluated at runtime. But in practice they‘re pretty much the same

the readonly modifier is applicable only for fields, and can‘t be used for local variables

关于函数重载
C++中可以出现同名函数,只要参数个数、参数类型和参数顺序不同,都认作是函数重载,注意返回值类型不同不可以算函数重载

ref和out关键字
ref跟out差不多,传入参数时不再传值,而是传引用,ref传入的参数必须初始化,但out的不需要。



二十. delegate和event

假设有两个类A和B,A负责获取外界的information,然后传递给B,为了实现这种功能,有以下做法:

  • A中设计一个bool值,用来表示是否获取到了新的外界信息,B不断去check这个布尔值,如果为true,那么B去调用A的GetInfomation()函数,这种方法叫做polling
  • B中设计一个方法,当A接收到数据时,直接调用B的对应的方法

A需要在得到数据的第一时间,把数据传给B,这一场景,C#提供了相应的keyword,叫做event,于是做法就变成了:

  • A定义一个event
  • B定义一个event handler,也就是一个函数,然后把这个event handler登记到A的event里

然后具体怎么写呢,先在A中定义对应的event:

class A{
    
    
	// EventHandler是C#里提供的一个委托, 对应的函数签名为void(object, EventArgs)
	// Event 类似于观察者模式中的被观察者(或者类比于主播)
	public event EventHandler InformationAlert;
}

注意这里的EventHandler并不是我自己定义的类型,而是C#的.Net里固有的东西,代码如下所示:

namespace System
{
    
    
    //
    // Summary:
    //     Represents the method that will handle an event that has no event data.
    //
    // Parameters:
    //   sender:
    //     The source of the event.
    //
    //   e:
    //     An object that contains no event data.
    public delegate void EventHandler(object sender, EventArgs e);
}

可以看到,EventHandler就是一个委托函数,第一个参数是object类型,也就是任意的C#对象,第二个参数EventArgs同样是C#的System里提供的类型,是C#里很多用于Event连接的类的基类( it is the base class for many derived classes that are used in connection with events)

所以这句话public event EventHandler InformationAlert什么意思呢,event参数表明这是一个event,EventHandler规定了,被通知的类(比如B),定义的EventHandler的函数签名。也就是说,B要想收到A的event的通知,必须声明以下函数:

// 观察者(或者说订阅主播的人), 必须是这种格式的函数
void MyInformationAlertHandler(object sender, EventArgs e)//名字不重要,但是必须是void返回值,参数是object和EventArgs
{
    
    
// process the event
}

OK,A的event声明好了,B的EventHandler也声明好了,剩下的就是将B登记到A里了,类似于观察者模式里的,Subject.List.AttachObserver的方式,登记EventHandler和解除登记的写法如下:

// 注意这里是用的A的实例a进行调用的
a.InformationAlert += new EventHandler(MyInformationAlertHandler);
// 解除登记
a.InformationAlert -= new EventHandler(MyInformationAlertHandler);

.NET 2.0以后有了更简单的写法,如下所示:

// 登记
a.InformationAlert += MyInformationAlertHandler;
// 解除登记
a.InformationAlert -= MyInformationAlertHandler;

那么,当A的对象a的event产生的时候,就会通知所有登记过的对象,调用各自的EventHandler:

//注意, 这是event通知订阅者的写法
if(InformationAlert != null)
	InformationAlert(this, new EventArgs());//就会把a本身的event和new出来的EventArgs作为参数传给相应的EventHandler

注意这里的,EventArgs只是一个参数,如果想要传多个参数,则需要创建继承于EventArgs的子类

event常常用于处理外界的Input,比如鼠标点击、键盘按键等,这里举个更具体的例子,C#的.NET系统设计了一个计时器(类似于古代打更的更夫),隔一段时间可以给予一个通知,叫做Timer。Timer本身就是一个event,叫做Elapsed,意思是时间流逝的意思。

更夫的名字叫Elapsed,实现类是Timer,定义在Timers的命名空间下,代码如下:

namespace System.Timers
{
    
    
	public class Timer : Component, ISupportInitialize
    {
    
    
		//一个用于通知的更夫,通知的方式是通过ElapsedEventHandler对应的函数签名, 对应的函数对象
		public event ElapsedEventHandler Elapsed;
		...//其他的代码省略
	}
}

然后查看EventHandler的定义,如下所示:

namespace System.Timers
{
    
    
    // Represents the method that will handle the System.Timers.Timer.Elapsed event
    // of a System.Timers.Timer.
    public delegate void ElapsedEventHandler(object sender, ElapsedEventArgs e);// 函数签名是最通用的那种
}

这里我有点疑问,C#可以这样单独把一个delegate,不放在任何类里暴露出来吗?

查阅资料后发现,定义委托也就是定义一个类。所以,只要是可以定义类的地方,都可以定义委托。委托既可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。委托和类、接口类似,通常比较多的放在类的外部或命名空间中。

所以是可以在类外定义委托的,比如我下面的代码可以成功运行:

public delegate void ElapsedEventHandler(object sender, ElapsedEventArgs e);
namespace ConsoleApp1
{
    
    
    class Program
    {
    
    
        public static void Main()
        {
    
    
            Console.WriteLine("Hello World");
        }
    }
}

继续写上面的例子,要实现这样一个功能,每隔一秒钟打印出当前的时间,代码如下所示:

namespace ConsoleApp1
{
    
    
    class Program
    {
    
    
        public static void Main()
        {
    
    
            Timer timer = new Timer();
            // Elapsed属性是一个event类型
            timer.Elapsed += MyEventHandler;
            timer.Interval = 1000;//ms
            timer.Enabled = true;

            Console.ReadLine();
            timer.Elapsed -= MyEventHandler;
        }

        //public delegate void EventHandler(object sender, ElapsedEventArgs e);
		
		// 函数签名必须与event声明时候的EventHandler的签名相同,这里的static是为了方便Main函数调用
        static void MyEventHandler(object sender, ElapsedEventArgs args)
        {
    
    
            Console.Write("\r{0} ", args.SignalTime.ToLongTimeString());// \r是转义字符,每次从行首开始复写
        }
    }
}

然后就能看到屏幕不断打印出当前的时间,显示上午10点29分39秒,每隔一秒更新一次,如下图所示:
在这里插入图片描述

.NET 2.0开始可以使用类似Lambda表达式的方式,去简化这个过程,写法如下:

static void Main()
{
    
    
	Timer tmr = new Timer();
	tmr.Elapsed += delegate(object sender, ElapsedEventArgs args)
	{
    
    
		Console.Write("\r{0} ", args.SignalTime.ToLongTimeString());
	};
	tmr.Interval = 1000;
	tmr.Enabled = true;
	Console.ReadLine();
}

如果不需要任何参数,还可以再进行简化:

tmr.Elapsed += delegate
{
    
    
	Console.Write("\r{0} ", DateTime.Now.ToLongTimeString());//利用DateTime类打印出当前时间
};

前面说的是C#自己的Event,坦白说,其中的interval怎么和event交互的,书里还是没有介绍。现在再看个例子,这个例子是为了实现,当一个参数发生改变的时候,立马作为事件,调用对应的处理函数,代码如下所示:

class Program
{
    
    
    // 定义事件发生时,调用的回调函数的签名方式:void类型,2个int类型参数
    public delegate void argChangedEventHandler(int oldVal, int newVal);

    class Example
    {
    
    
            // 为这个类定义一个event,名字叫PriceChangedObserver, 通知的对象是函数, 函数签名是void(int, int)
            public event argChangedEventHandler PriceChangedObserver;
			// 类对象有一个公开的回调函数,可以等待被赋值,其实就是一个委托对象等待被赋值
            public argChangedEventHandler whenChangedDoSomething;
            private int _price;

            public Example(){
    
     _price = 0; }

            public int Price
            {
    
    
                set
                {
    
    
                	// 在Set属性里,去判断,如果与原来的值不一样,则调用类对象内部记载的回调函数
                    if (_price != value)
                    {
    
    	
                    	// 当值改变时, 去调用事件对应的委托, value是原本的值
                        whenChangedDoSomething?.Invoke(_price, value);  //trigger Event when Price was changed
                    }
                    _price = value;
                }
            }
  	}

    public static void PrintWhenPriceChanged(int oldVal, int newVal)
    {
    
    
        Console.WriteLine("Old value is :" + oldVal);
        Console.WriteLine("New value is :" + newVal);
    }

    static void Main(string[] args)
    {
    
    
        Example item = new Example();
        item.whenChangedDoSomething += PrintWhenPriceChanged;
        item.Price = 123;   //会去调用对应的回调函数
    }
}

最后打印如下:

Old value is :0
New value is :123
请按任意键继续. . .


二十一. new keyword

C++的new正常是会调用全局的operator new函数,类下可能会调用类自己重载的operator new函数,还有placement new等用法,而C#里的new比这个就简单多了,对于struct或者class,new关键字承担了初始化的工作,对于类内的值类型变量,都会置为0,对于引用类型变量,都会置为null,举个例子,代码如下所示:

 struct MyStruct
 {
    
    
 	public int a;
}

public static void Main()
{
    
    
	MyStruct s1 = new MyStruct();
	MyStruct s2;
	Console.WriteLine(s1.a);// 正确,a被new初始化为0
	Console.WriteLine(s2.a);// 编译报错,a没有初始化
}

再考虑一下内存的问题,看看下面一行代码:

Data myData;

这一行语句只会在stack上面分配内存,如果Data是Struct类型,那么Stack上分配的内存就是Data的一个对象的大小,比如Data如果有三个interger,就是分配12个字节,如果Data是引用类型,则分配的是一个引用,或者说指针,一共四个字节。

myData = null;//只有当myData是Class类型的时候才可以这么写
myData = new myData();//如果是Struct,则这里进行了初始化,如果是Class类型,则在Heap上分配了内存,并进行了初始化

如果用的是new [],那就不一样了,C#里的数组总是分配在Heap上的,如下所示:

dates = new Date[27];

对于上面这句话,如果Data是Struct类型,那么会立马在Heap上分配27个Instance的大小,而且每个Instance都会被初始化,而对于Class类型,只会在Heap上分配27个Reference的大小,,对应的引用都是null,具体的Instance处于未被创建的状态,需要单独去new才能创建具体的对象。

Struct类型的内存消耗更小,适合存放Vector3f这种类似interger的小数据类型



二十二. Boxing and Unboxing

Boxing causes the value to be copied from the Stack, put in an Object, and placed on the Heap

decimal pi = 3.14159m;
object obj = pi;

上述代码进行了Boxing的行为,具体在内存上的行为是,当decimal进行装箱时,会在Heap分配对应的内存,然后把pi从stack上复制存储到heap中,Heap上除了分配内存去存储其对应的值外,还分配的内存用来存储装箱过程原来的值对应的类型,方便后面进行Unboxing,拆箱的过程则正好相反,是从Heap上复制内存到Stack上,注意以上两个过程都是复制,原本的内容都还在,可以看下面两个例子

class Program
{
    
    
    struct Point1
    {
    
    
        public int x, y;
        public Point1(int x, int y)
        {
    
    
            this.x = x;
            this.y = y;
        }
    }

    class Point2
    {
    
    
        public int x, y;
        public Point2(int x, int y)
        {
    
    
            this.x = x;
            this.y = y;
        }
    }

    public static void Main()
    {
    
    
        Point1 p1 = new Point1(1, 1);
        object o1 = p1;
        p1.x = 2;
        Console.WriteLine(((Point1)o1).x);// Print 1, 因为装箱过程只是一个复制,创建了新的对象,不会影响原来的对象

        Point2 p2 = new Point2(1, 1);
        object o2 = p2;
        p2.x = 2;
        Console.WriteLine(((Point2)o2).x);// Print 2
    }
}

什么时候需要Boxing
参考链接:https://stackoverflow.com/questions/2111857/why-do-we-need-boxing-and-unboxing-in-c#:~:text=Boxing%20and%20Unboxing%20are%20specifically,types%20as%20instances%20of%20Object.

There are some basic structures in .Net that still demand passing Value Types as object (and so require Boxing), but for the most part you should never need to Box

感觉是尽量不要用Boxing,不过.NET的一些历史遗留问题会导致不得不使用Boxing,主要有以下三点:

  • The Event system turns out to have a Race Condition in naive use of it, and it doesn’t support async. Add in the Boxing problem and it should probably be avoided. (You could replace it for example with an async event system that uses Generics.)
  • The old Threading and Timer models forced a Box on their parameters but have been replaced by async/await which are far cleaner and more efficient.
  • The .Net 1.1 Collections relied entirely on Boxing, because they came before Generics. These are still kicking around in System.Collections. In any new code you should be using the Collections from System.Collections.Generic, which in addition to avoiding Boxing also provide you with stronger type-safety.

翻译过来就是:

  • 关于C#的Event和Race Condition里面会用到,Remain
  • 一些老的Thread和Timer类会需要这个,不过可以用async/await函数解决这个问题
  • 老的.NET 1.1的容器会用到Boxing,代码如下所示:
// 应该用Generic命名空间下的容器
using System.Collections.Generic;

var employeeCount = 5;
var list = new List<int>(10);

// 而下面的容器就尽量少用
using System.Collections;

Int32 employeeCount = 5;
var list = new ArrayList(10);


二十三. Action和Func

其实是一个很简单的东西,进去看可以看到这样的定义:

namespace System
{
    
    
    //
    // Summary:
    //     Encapsulates a method that has no parameters and does not return a value.
    public delegate void Action();
}

其实就是帮我们定义了一个回调函数的函数签名而已,这个函数没有参数,返回类型为void,举个使用的例子:

void func(Param param, Action action)
{
    
    
	// Do something
	// ...
	
	action();//在做完对应的处理后,调用传进来的这个回调函数action
}

如果想要为Action对应的回调函数加上参数,可以这么写:

static void ConsolePrint(int i)
{
    
    
    Console.WriteLine(i);
}

static void Main(string[] args)
{
    
    
    Action<int> printActionDel = ConsolePrint;
    printActionDel(10);
}

注意这里的Action返回类型是void,如果想返回内容,就不能用Action了,而是要用Func参数,其定义如下:

namespace System
{
    
    
    //
    // Summary:
    //     Encapsulates a method that has no parameters and returns a value of the type
    //     specified by the TResult parameter.
    //
    // Type parameters:
    //   TResult:
    //     The type of the return value of the method that this delegate encapsulates.
    //
    // Returns:
    //     The return value of the method that this delegate encapsulates.
    public delegate TResult Func<out TResult>();
}

猜你喜欢

转载自blog.csdn.net/alexhu2010q/article/details/105317397
今日推荐