C#:C# 是专为公共语言基础结构(CLI)设计的。CLI 由可执行代码和运行时环境组成,允许在不同的计算机平台和体系结构上使用各种高级语言。
C#/.NET CORE/.NET Framework/.NET Standard
C#程序员可以不去太在意底层的实现,大胆的在.NET提供的平台上大放异彩。但是,我是说最好,还是对他了解一点:
对比C/Cpp等非托管代码,C#的托管代码到达操作系统的过程是怎样的呢?
先来认识几个名词:
Just In tIME JIT JIT编译器
Common Intermediate Language CIL公共中间语言。
而.NET编译代码的时候会把高级编程语言编译成中间语言CIL运行在CLR上,也就是把代码集成一个exe文件中,
.NET 在编译过程中 没有直接编译成CPU认识的代码,而是编译成了CLR所认识的代码,这为跨平台奠定了基础。
比如我们在C语言中编写的代码要移植到C#中 C#中只要有相应的编译器(JIT) 这时候就能直接编译从另一种语言编译过来的CLR也就是exe文件。
总的来说,编译过程就是把用户看得懂的语言比如 Console.WriteLine(“hello,world”);编程成CLR认识的代码也就是集成exe文件。
然后由CLR编译成CPU所认识的0和1.
C/CPP/C#的变量定义都是一样的直接,是程序可操作的存储区的名词,每个变量都有特定的类型,决定了变量存储的大小和布局
C#的数据类型主要是三部分:
可以直接申请一个值类型变量并存入值,与C/CPP不同的是:
值类型变量是一个个对象,不同的值类型都是由System.ValueType派生来的。(栈上分配内存)
引用类型继承自System.Object类,System.ValueType的父类。System.ValueType本身是一个类类型,而不是值类型。其关键在于ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。结构体struct直接派生于System.ValueType;(托管堆上分配内存)
值类型各种整型(System.Int16),浮点型(System.Double),bool型System.Boolean的别名),枚举派生于System.Enum),可空类型(派生于System.Nullable泛型结构体,T?实际上是System.Nullable的别名)
每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值。例如:
C# 提供了一个特殊的数据类型,nullable 类型(可空类型),可空类型可以表示其基础值类型正常范围内的值,再加上一个 null 值。
例如,Nullable< Int32 >,读作"可空的 Int32",可以被赋值为 -2,147,483,648 到 2,147,483,647 之间的任意值,也可以被赋值为 null 值。类似的,Nullable< bool > 变量可以被赋值为 true 或 false 或 null。
在处理数据库和其他包含可能未赋值的元素的数据类型时,将 null 赋值给数值类型或布尔型的功能特别有用。例如,数据库中的布尔型字段可以存储值 true 或 false,或者,该字段也可以未定义。
格式: < data_type> ?= null;
int i = new int();
等价于:
Int32 i = new Int32();
引用类型:数组(派生于System.Array),类派生于System.Object),委托派生于System.Delegate),字符串(System.String的别名)
注意:
引用类型可以派生出新的类型,而值类型不能;
引用类型可以包含null值,值类型不能(可空类型功能允许将 null 赋给值类型);
引用类型变量的赋值只复制对对象的引用,而不复制对象本身。而将一个值类型变量赋给另一个值类型变量时,将复制包含的值。
string s1 = "a";
string s2 = s1;
s1 = "b";//s2 is still "a"
改变s1的值对s2没有影响。这更使string看起来像值类型。实际上,这是运算符重载的结果,当s1被改变时,.NET在托管堆上为s1重新分配了内存。这样的目的,是为了将做为引用类型的string实现为通常语义下的字符串。
(注:@"C:\Windows"这样的@"xxxx"表示将转义字符\当做普通字符看待)
C#支持将值类型与引用类型互相转换:
装箱 —把值类型强制转换成引用类型(object类型)
拆箱 —把引用类型强制转换成值类型,这个过程也称之为"强制转换"
C#中值类型和引用类型的最终基类都是Object类型(它本身是一个引用类型)也就是说,值类型也可以当做引用类型来处理。
int num = 100;
object obj = val; //因为值类型的老祖宗就是object类型 装箱,将值类型转化为引用类型。
int val = 100;
object obj = val;
int num = (int) obj;
Console.WriteLine ("num: {0}", num); //此为拆箱,被装箱的对象才能被拆箱。
//将值转换为引用类型,再由引用类型转换为值类型。
图解装箱
对值类型在堆中分配一个对象实例,并将该值复制到新的对象中。按三步进行。
1:首先从托管堆中为新生成的引用对象分配内存(大小为值类型实例大小加上一个方法表指针和一个SyncBlockIndex)。
2:然后将值类型的数据拷贝到刚刚分配的内存中。
3:返回托管堆中新分配对象的地址。这个地址就是一个指向对象的引用了。
可以看出,进行一次装箱要进行分配内存和拷贝数据这两项比较影响性能的操作。
1、首先获取托管堆中属于值类型那部分字段的地址,这一步是严格意义上的拆箱。
2、将引用对象中的值拷贝到位于线程堆栈上的值类型实例中。
经过这2步,可以认为是同boxing是互反操作。严格意义上的拆箱,并不影响性能,但伴随这之后的拷贝数据的操作就会同boxing操作中一样影响性能。
指针类型,和C/CPP上的区别:
1,使用指针时需要用unsafe关键字来修饰含有指针操作的程序段,unsafe只是编译器指令语法,只是在编译的时候起作用
2,定义指针时语法上有所不同:
C++中*是和其后面的变量名结合的:
int *p1,*p2; //代表p1,p1都是指向int类型的指针
int *p1,p2; //代表p1是指针,p2是int类型变量
而C#中*是和其前面的类型名结合的:
int* p1,p2; //代表p1,p2都是指针
3,C#指针类型不能是引用类型,不能是含有引用类型的自定义类型或复合类型。只能是预定义的类型
string * pM; //C#中string是引用类型 X
struct ServiceStatus
{
int State;
string Description; // 引用类型
}
structStatus* pStatus //含有引用类型的自定义类型 X
4, 在C#里,引用类型也叫Movable类型,由于垃圾回收器和内存布局优化,在堆上分配的内存是不固定的,会被挪动。所不能将堆上分配的地址赋值给C#指针, 如果能将堆上地址固定,就没有问题了,那么如何固定引用类型?C#中有一个关键字 fixed可以用在此处。
C#有Null合并运算符(??)
Null 合并运算符用于定义可空类型和引用类型的默认值。Null 合并运算符为类型转换定义了一个预设值,以防可空类型的值为 Null。Null 合并运算符把操作数类型隐式转换为另一个可空(或不可空)的值类型的操作数的类型。
如果第一个操作数的值为 null,则运算符返回第二个操作数的值,否则返回第一个操作数的值。下面的实例演示了这点:
double? num1 = null;
double? num2 = 3.14157;
double num3;
num3 = num1 ?? 5.34;
Console.WriteLine("num3 的值: {0}", num3);
num3 = num2 ?? 5.34;
Console.WriteLine("num3 的值: {0}", num3);
Console.ReadLine();
============================================
num3 的值: 5.34
num3 的值: 3.14157
稍有不同,主要是foreach,可以迭代数组或者一个集合对象。类似于python的语法
foreach (var v in list)
{
}
1.声明格式变了
datatype[] arrayName;
datatype表示数组元素的类型
[]里指定数组的维度
arrayName是数组的标识
如果要声明多维数组:
int [ , , ] m;
访问时:
/* 一个带有 5 行 2 列的数组 */
int[,] a = new int[5, 2] {{0,0}, {1,2}, {2,4}, {3,6}, {4,8} };
int i, j;
/* 输出数组中每个元素的值 */
for (i = 0; i < 5; i++)
{
for (j = 0; j < 2; j++)
{
Console.WriteLine("a[{0},{1}] = {2}", i, j, a[i,j]);
}
}
2.概念变了
C#的数组是引用类型,必须用new来创建数组实例
结构可带有方法、字段、索引、属性、运算符方法和事件。
结构可定义构造函数,但不能定义析构函数。但是,您不能为结构定义默认的构造函数。默认的构造函数是自动定义的,且不能被改变。
与类不同,结构不能继承其他的结构或类。
结构不能作为其他结构或类的基础结构。
结构可实现一个或多个接口。
结构成员不能指定为 abstract、virtual 或 protected。
当您使用 New 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 New 操作符即可被实例化。
如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。
using时不需要再写namespace,声明时相同
using System
通过关键字 operator 后跟运算符的符号来定义
public static Box operator+ (Box b, Box c)
{
Box box = new Box();
box.length = b.length + c.length;
box.breadth = b.breadth + c.breadth;
box.height = b.height + c.height;
return box;
}
C# 中的异常类主要是直接或间接地派生于 System.Exception 类。
System.ApplicationException 和 System.SystemException 类是派生于 System.Exception 类的异常类。
System.ApplicationException 类支持由应用程序生成的异常。所以程序员定义的异常都应派生自该类。
System.SystemException 类是所有预定义的系统异常的基类。
一个 文件 是一个存储在磁盘中带有指定名称和目录路径的数据集合。当打开文件进行读写时,它变成一个 流。
从根本上说,流是通过通信路径传递的字节序列。有两个主要的流:输入流 和 输出流。输入流用于从文件读取数据(读操作),输出流用于向文件写入数据(写操作)。
C# I/O 类
System.IO 命名空间有各种不同的类,用于执行各种文件操作,如创建和删除文件、读取或写入文件,关闭文件等。
详情查询手册
C#的方法和Cpp的方法很像,在参数传递上有较大的不同:
有三种向方法传递参数的方式:
值传参和C/Cpp一样
引用传递参数定义是这样的:
引用参数是一个对变量的内存位置的引用。当按引用传递参数时,与值参数不同的是,它不会为这些参数创建一个新的存储位置。引用参数表示与提供给方法的实际参数具有相同的内存位置。
引用传递机制和Cpp传引用一样
形式上稍有不同:在 C# 中,使用 ref
关键字声明引用参数。
publicvoid swap(ref int x,ref int y)
{
int temp;
temp = x;/* 保存 x 的值 */
x = y;/* 把 y 赋值给 x */
y = temp;/* 把 temp 赋值给 y */
}
按输出传递
return
语句可用于只从函数中返回一个值。但是,可以使用 输出参数 来从函数中返回两个值。输出参数会把方法输出的数据赋给自己,其他方面与引用参数相似。
说白了就是用out
修饰符表明传入的变量可以从函数中捞数据,而不是像函数中的局部变量在函数结束时就生命期结束了
提供给输出参数的变量不需要赋值。当需要从一个参数没有指定初始值的方法中返回值时,输出参数特别有用。
类基本和Cpp一样,但是C#中没有明确区别类的声明和类的定义两个概念,当然有C/Cpp的背景的程序员是会明确区别出这两者的,C/Cpp中累的声明和定义一般是分开的,.h中声明,.c中去实现定义(也可以放在一起),如果一个函数或者方法只有声明没有定义且程序中又使用了该方法,则编译器可以通过,链接器会报错。
访问修饰符:
对外公开时,类成员的最高访问级别受类的访问级别所限制,如果类是internal的,则尽管类成员是public,其他程序集也无法访问到
子类虽然可以继承父类的private成员,但是却无法直接访问,父类可以留一个public的能够访问private成员的方法给子类继承,从而使他能够间接访问。
在团队合作当中,不像让别人看到的方法最好不要public暴露出来,想暴露给子类可以用protected。
基本与Cpp继承相同,C#继承是单根,一个类只能继承另一个类,而不能同时继承多个(但是可以实现多个基接口)
继承是面向对象程序设计中最重要的概念之一。继承允许我们根据一个类来定义另一个类,这使得创建和维护应用程序变得更容易。同时也有利于重用代码和节省开发时间。
基类的本质就是派生类在基类已有的成员基础上对基类进行横向和纵向的扩展。
横向:数量上继承时永远向下扩充 类变胖了 只能增加不能移除,这一点Python可以
纵向:版本上继承时永远向下更新 类变新了
继承的思想实现了 属于(IS-A) 关系。例如,哺乳动物 属于(IS-A) 动物,狗 属于(IS-A) 哺乳动物,因此狗 属于(IS-A) 动物。
于是就可以使用一种强大的功能:用父类对象来引用子类的实例。多态中很好用。
Vehicle v = new Car();
Object o1 = new Vehicle();
Object o2 = new Car();
基类构造器无法被继承
基类构造器先被触发,然后一级一级往下构造,可以打断点来证明
base只能访问上一级,无法base.base.field
基类的初始化很重要:派生类继承了基类的成员变量和成员方法。因此父类对象应在子类对象创建之前被创建。您可以在成员初始化列表中进行父类的初始化。
在继承中,子类可以在构造函数中显式调用父类的构造函数,来初始化父类的成员。
public Tabletop(double l, double w) : base(l, w)
如果没有显式调用,那么编译器会自动帮你调用父类的默认构造函数。因为编译器非常傻,所以,在自动调用的过程中可能会面临着一些问题。
1.父类没有声明构造函数,子类没有显式调用父类构造函数
父类没有声明构造函数,则编译器会为他生成一个默认无参构造函数。子类没有显式调用父类构造函数,编译器就会为他调用父类的默认无参构造函数。
2.父类声明了有参构造函数,子类没有显式调用父类构造函数
父类声明了有参构造函数,则编译器不会为他生成无参构造函数,子类没有显式调用父类构造函数,也没有无参构造函数可以调自动用,则会报错。
另一种解决方式:父类构造函数使用默认参数
重写是继承的基础
同一个方法,父类用virtual修饰子类用override修饰,那就是在纵向扩展这个run方法,即版本更新了,此后子类再怎么多态都访问不到父类的run方法了,只能表现出继承链上最新版本的run方法(也可以理解为把父类那里继承的方法给擦掉了,自己又重新写了一个)
但是
如果都不加上面两个修饰符,那就是隐藏:也就是说子类其实继承了父类run方法,但是把它隐藏掉了,用户自己又添加了一个run方法。即子类这时有两个run方法。(可以理解为把父类那里继承的方法遮住了,自己在旁边又写了一个)
vs会给出提示:
此时如果用父类的身份去访问run方法,还是可以访问到的。这就是隐藏和重写的区别。
一个接口,多个功能
分为静态多态性和动态多态性
静态:函数重载和运算符重载
动态:
1,使用abstract关键字创建抽象类,用于提供接口的部分类的实现。当一个派生类继承自该抽象类时,实现即完成。抽象类包含抽象方法,抽象方法可被派生类实现。派生类具有更专业的功能。
2,使用virtual关键字创建虚方法,该方法可以在继承类中实现,可以在不同的继承类中有不同的实现。
您不能创建一个抽象类的实例。
您不能在一个抽象类外部声明一个抽象方法。
通过在类定义前面放置关键字 sealed,可以将类声明为密封类。当一个类被声明为 sealed 时,它不能被继承。抽象类不能被声明为
sealed。
C#的属性是一种比字段更加强大的存在。对一个对象的字段可以进行访问和赋值,而属性则是字段的扩展,我们可以通过与字段相同的语法来访问,但是由于有语法糖加持,这种访问内部会使用访问器:
using System;
namespace tutorialspoint
{
class Student
{
private string code = "N.A";
private string name = "not known";
private int age = 0;
// 声明类型为 string 的 Code 属性
public string Code
{
get
{
return code;
}
set
{
code = value;
}
}
// 声明类型为 string 的 Name 属性
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
// 声明类型为 int 的 Age 属性
public int Age
{
get
{
return age;
}
set
{
age = value;
}
}
public override string ToString()
{
return "Code = " + Code +", Name = " + Name + ", Age = " + Age;
}
}
class ExampleDemo
{
public static void Main()
{
// 创建一个新的 Student 对象
Student s = new Student();
// 设置 student 的 code、name 和 age
s.Code = "001";
s.Name = "Zara";
s.Age = 9;
Console.WriteLine("Student Info: {0}", s);
// 增加年龄
s.Age += 1;
Console.WriteLine("Student Info: {0}", s);
Console.ReadKey();
}
}
}
=========================================
result:
Student Info: Code = 001, Name = Zara, Age = 9
Student Info: Code = 001, Name = Zara, Age = 10
其实这种写法就是为了封装数据,Cpp与JAVA,python中也可以实现,只是C#的实现更加优雅一点。
由于要形成多态,势必会形成虚方法,这样的方法在父类中是永远无法被调用到的(因为已经覆盖了),虚方法是实现了逻辑但在子类中会被更新,有名无实。所以他其实没什么用,于是就有了将他制作的更加干净的需求:直接把方法体的括号去掉,并用abstract修饰,当然与此同时该方法所在的类也得用abstract修饰,此时抽象类的概念就出来了。抽象类就是下推给子类实现的一种强制做法。
更极端的,如果一个类中所有的方法被用abstract修饰,此时就形成了纯抽象类,他实际上就是接口了,只需要将abstract都替换成interface,并且把public修饰符都去掉。
接口定义了所有类继承接口时应遵循的语法合同/契约。接口定义了语法合同 “是什么” 部分,派生类定义了语法合同 “怎么做” 部分。
Contract
接口定义了属性、方法和事件,这些都是接口的成员。接口只包含了成员的声明。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。
接口使得实现接口的类或结构在形式上保持一致。
抽象类在某种程度上与接口类似,但是,它们大多只是用在当只有少数方法由基类声明由派生类实现时。
接口中的函数成员是隐式public的,不必写出来,写出来会报错。
规则:
如果一个接口继承其他接口,那么实现类或结构就需要实现所有接口的成员。
using System;
interface IParentInterface
{
void ParentInterfaceMethod();
}
interface IMyInterface : IParentInterface
{
void MethodToImplement();
}
class InterfaceImplementer : IMyInterface
{
static void Main()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
iImp.ParentInterfaceMethod();
}
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
public void ParentInterfaceMethod()
{
Console.WriteLine("ParentInterfaceMethod() called.");
}
}
作用;
(1)通过接口可以实现不相关类的相同行为,而不需要了解对象所对应的类。
(2)通过接口可以指明多个类需要实现的方法。
(3)通过接口可以了解对象的交互界面,而不需了解对象所对应的类。
你当然也可以不用接口,直接在类里面写方法,但是如果你的一组方法需要在很多类里实现,那么把它们抽象出来,做成一个接口规范,不是更好么?
抽象类里有部分 未实现方法体的方法,所以抽象类不能拿来使用,他的作用有两个:
1,作为基类给别的类去继承,子类会去实现他内部未实现的方法
2,作为基类去作为子类对象的引用,实现多态效果。
抽象方法有点像是被子类所重写的,而且抽象方法的方法体是空的,所以又被叫做纯虚方法
抽象类可拥有抽象属性,这些属性应在派生类中被实现。
注意下面修饰Name属性的abstract 和 修饰Name属性实现的override
using System;
namespace tutorialspoint
{
public abstract class Person
{
public abstract string Name
{
get;
set;
}
public abstract int Age
{
get;
set;
}
}
class Student : Person
{
private string code = "N.A";
private string name = "N.A";
private int age = 0;
// 声明类型为 string 的 Code 属性
public string Code
{
get
{
return code;
}
set
{
code = value;
}
}
// 声明类型为 string 的 Name 属性
public override string Name
{
get
{
return name;
}
set
{
name = value;
}
}
// 声明类型为 int 的 Age 属性
public override int Age
{
get
{
return age;
}
set
{
age = value;
}
}
public override string ToString()
{
return "Code = " + Code +", Name = " + Name + ", Age = " + Age;
}
}
class ExampleDemo
{
public static void Main()
{
// 创建一个新的 Student 对象
Student s = new Student();
// 设置 student 的 code、name 和 age
s.Code = "001";
s.Name = "Zara";
s.Age = 9;
Console.WriteLine("Student Info:- {0}", s);
// 增加年龄
s.Age += 1;
Console.WriteLine("Student Info:- {0}", s);
Console.ReadKey();
}
}
}
======================================================
接口和抽象类是“软件工程产物”
具体类–>抽象类–>接口:越来越抽象,内部实现越来越少
抽象类是未完全实现逻辑的类(可以有字段和非public成员,他们代表了“具体逻辑”)
抽象类为了复用而生:专门作为基类来使用,也具有解耦功能
封装确定的,开放不确定的,推迟到合适的子类中去实现
接口是完全未实现逻辑的“类”(“纯虚类”;只有函数成员;成员全部public)
接口为解耦而生:“高内聚。低耦合”,方便单元测试
接口是一个“协约”,有分工必有协作,有协作必有协约
他们都不能实例化,只能用来声明变量,引用具体类的使用
=======================================================
是什么?
索引器(Indexer) 允许一个对象可以像数组一样被索引。当您为类定义一个索引器时,该类的行为就会像一个 虚拟数组(virtual array) 一样。您可以使用数组访问运算符([ ])来访问该类的实例。
能有什么作用?
索引器的行为的声明在某种程度上类似于属性(property)。就像属性(property),您可使用 get 和 set 访问器来定义索引器。但是,属性返回或设置一个特定的数据成员,而索引器返回或设置对象实例的一个特定值。换句话说,它把实例数据分为更小的部分,并索引每个部分,获取或设置每个部分。
怎么写?
定义一个属性(property)包括提供属性名称。索引器定义的时候不带有名称,但带有 this 关键字,它指向对象实例。下面的实例演示了这个概念:
using System;
namespace IndexerApplication
{
class IndexedNames
{
private string[] namelist = new string[size];
static public int size = 10;
public IndexedNames()
{
for (int i = 0; i < size; i++)
namelist[i] = "N. A.";
}
public string this[int index]
{
get
{
string tmp;
if( index >= 0 && index <= size-1 )
{
tmp = namelist[index];
}
else
{
tmp = "";
}
return ( tmp );
}
set
{
if( index >= 0 && index <= size-1 )
{
namelist[index] = value;
}
}
}
static void Main(string[] args)
{
IndexedNames names = new IndexedNames();
names[0] = "Zara";
names[1] = "Riz";
names[2] = "Nuha";
names[3] = "Asif";
names[4] = "Davinder";
names[5] = "Sunil";
names[6] = "Rubic";
for ( int i = 0; i < IndexedNames.size; i++ )
{
Console.WriteLine(names[i]);
}
Console.ReadKey();
}
}
}
==============================================
Zara
Riz
Nuha
Asif
Davinder
Sunil
Rubic
N. A.
N. A.
N. A.
可见索引器的作用就是提供了一种通过 对象名[index] 来对内部的数组属性设置的手段
如果不是要去修bug或者加功能,闲的没事儿就别去修改类成员的代码。
应该封装那些不变的,稳定的,确定的成员,把那些不稳定的成员声明为抽象类,留给子类去实现。
(开闭原则-抽象类,天生一对)(如无必要,别动代码)
不能copy paste