类包含成员,成员可以是静态成员或实例成员。静态成员属于类,实例成员属于对象。静态字段的值对每
个对象都是相同的。而每个对象的实例字段都可以有不同的值。静态成员关联了static修饰符。成员的种类如下

字段是与类相关的变量,一旦实例化PhoneCustomer对象,就可以使用语法Object.FieldName来访问这些字段,如下例所示:
namespace Demo
{
class Program
{
static void Main()
{
var customer1 = new PhoneCustomer();
customer1.FirstNanme = "Simon";
Console.WriteLine(customer1.FirstNanme);
}
}
}
public class PhoneCustomer
{
public string FirstNanme;
}
为了保证对象的字段不能改变,字段可以用readonly修饰符声明。带有readonly修饰符的字段只能在构造函数中分配值。它与const修饰符不同。编译器通过const修饰符,用其值取代了使用它的变量。编译器知道常量的值。只读字段在运行期间通过构造函数指定。与常量字段相反,只读字段可以是实例成员。使用只读字段作为类成员时,需要把static修饰符分配给该字段.
如果有一个用于编辑文档的程序,因为要注册,所以需要限制可以同时打开的文档数。
例如,假定编辑的每个文档都有一个创建日期,但不允许用户修改它(因为这会覆盖过去的日期)。
public class Document
{
private readonly DateTime _creationTime;
public Document()
{
_creationTime = DateTime.Now;
}
}
在上面的代码段中,_creationTime的处理方式与任何其他字段相同,但因为它们是只读的,所以不能在构造函数外部赋值
还要注意,在构造函数中不必给只读字段赋值。如果没有赋值,它的值就是其特定数据类型的默认值,或
者在声明时给它初始化的值。这适用于只读的静态字段和实例字段。
最好不把字段声明为public。如果修改类的公共成员,使用这个公共成员的每个调用程序也需要更改。例
如,如果希望在下一个版本中检查最大的字符串长度,公共字段就需要更改为一个属性。使用公共字段的现有
代码,必须重新编译,才能使用这个属性(尽管在调用程序看来,语法与属性相同)。如果只在现有的属性中改
变检查,那么调用程序不需要重新编译就能使用新版本。最好把字段声明为private,使用属性来访问字段.
属性(property)的概念是:它是一个方法或一对方法,在客户端代码看来,它(们)是一个字段。
下面把前面示例中变量名为firstName的名字字段改为私有。FirstName属性包含get和set访问器,来检和设置支持学段的值.
public class PhoneCustomer
{
private string _firstName;
public string FirstName
{
get
{
return _firstName;
}
set
{
_firstName = value;
}
}
}
get访问器不带任何参数,且必须返回属性声明的类型。也不应为set访问器指定任何显式参数,但编译器假
定它带一个参数,其类型也与属性相同,并表示为value。
下面的示例使用另一个命名约定。下面的代码包含一个属性Age,它设置了一个字段age。在这个例子中,age
表示属性Age的后备变量。
private int age;
public int Age
{
get
{
return age;
}
set
{
age = value;
}
}
使用C#7,还可以将属性访问器编写为具有表达式体的成员。例如,前面显示的属性FirstName可以使用
=>编写。这个新特性减少了编写大括号的需求,并且使用get访问器省略了return关键字。
private string _firstName;
public string FirstName
{
get => _firstName;
set => _firstName = value;
}
如果属性的set和get访问器中没有任何逻辑,就可以使用自动实现的属性。这种属性会自动实现后备成员变量。代码如下:
public int Age {get; set;};
不需要声明私有字段。编译器会自动创建它。使用自动实现的属性,就不能直接访问字段,因为不知道编译器生成的名称。如果对属性所需要做的就是读取和编写一个字段,那么使用自动实现属性时的属性语法比使
用具有表达式体的属性访问器时的语法要短。使用自动实现的属性,就不能在属性设置中验证属性的有效性。所以在上面的例子中,不能检查是否设置了无效的年龄。自动实现的属性可以使用属性初始化器来初始化
public int Age {get; set;}=42;
C#允许给属性的get和set访问器设置不同的访问修饰符,所以属性可以有公有的get访问器和私有或受保
护的set访问器。这有助于控制属性的设置方式或时间。
private string _firstName;
public string FirstName
{
get => _firstName;
private set => _firstName = value;
}
通过自动实现的属性,也可以设置不同的访问级别
public int Age {get; private set;};
在属性定义中省略set访问器,就可以创建只读属性。因此,如下代码把Name变成只读属性
private string _name;
private readonly string _name;
public string Name
{
get => _name;
}
用readonly修饰符声明字段,只允许在构造函数中初始化属性的值。
C#提供了一个简单的语法,使用自动实现的属性创建只读属性,访问只读字段。这些属性可以使用属性
初始化器来初始化。
public string Id { get;} == Guid.NewGuid().ToString();
在后台,编译器会创建一个只读字段和一个属性,其get访问器可以访问这个字段。初始化器的代码进入构造函数的实现代码,并在调用构造函数体之前调用。当然,只读属性也可以显式地在构造函数中初始化,如下面的代码片段所示:
public class Person
{
public Person(string name) => Name = name;
public string Name;
}
从C#6开始,只有get访问器的属性可以使用表达式体属性实现。类似于表达式体方法,表达式体属性不
需要花括号和返回语句。表达式体属性是带有get访问器的属性,但不需要编写get关键字。只是get访问器的
实现后跟lambda操作符。
public class Person
{
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FirstName { get; }
public string LastName { get; }
public string FullName => $"{FirstName} {LastName}";
}
如果类型包含可以改变的成员,它就是一个可变的类型。使用readonly修饰符,编译器会在状态改变时报
错。状态只能在构造函数中初始化。如果对象没有任何可以改变的成员,只有只读成员,它就是一个不可变类
型。其内容只能在初始化时设置。这对于多线程是非常有用的,因为多个线程可以访问信息永远不会改变的同
一个对象。因为内容不能改变,所以不需要同步。
不可变类型的一个例子是Sting类。这个类没有定义任何允许改变其内容的成员。诸如ToUpper(把字符串
更改为大写)的方法总是返回一个新的字符串,但传递到构造函数的原始字符串保持不变
var关键字,它用于表示隐式类型化的变量。var与new关键字一起使用时,可以创建名类型。匿名类型只是一个继承自Object且没有名称的类。该类的定义从初始化器中推断,类似于隐式类型化的
变量。如果需要一个对象包含某个人的姓氏、中间名和名字,则声明如下:
var captain = new
{
FirstName = "wJames",
MiddleName = "T",
LastName ="Kirk"
}
正式的C#术语区分函数和方法。在C#术语中,“函数成员”不仅包含方法,也包含类或结构的一些非数据成员,如索引器、运算符、构造函数和析构函数等,甚至还有属性。这些都不是数据成员,字段、常量和事件才是数据成员。
在C#中,方法的定义包括任意方法修饰符(如方法的可访问性)、返回值的类型,然后依次是方法名、输入
参数的列表(用圆括号括起来)和方法体(用花括号括起来)。
[modifiers] return_type MethodName([parameters])
{
// Method body
}
每个参数都包括参数的类型名和在方法体中的引用名称。但如果方法有返回值,则retun语句就必须与返
回值一起使用,以指定出口点,例如:
public bool IsSquare(Rectangle rect)
{
return (rect.Heightrect.width);
}
如果方法没有返回值,就把返回类型指定为void,因为不能省略返回类型。如果方法不带参数,仍需要在方法名的后面包含一对空的圆括号0。此时retum语句就是可选的一当到达闭花括号时,方法会自动返回。
如果方法的实现只有一条语句,C#为方法定义提供了一个简化的语法:表达式体方法。使用新的语法,不
需要编写花括号和returm关键字,而使用运算符=>区分操作符左边的声明和操作符右边的实现代码。
public bool IsSquare (Rectangle rect)=>rect.Heiaht == rect.width;
public class Math
{
public int Value { get; set; }
public int GetSquare() => Value * Value;
public static int GetSquareOf(int x) => x * x;
public static double GetPi() => 3.14159;
}
class Program
{
static void Main()
{
// 调用静态方法
Console.WriteLine($"Pi is {Math.GetPi()}");
int x = Math.GetSquareOf(5);
Console.WriteLine($"Square of 5 is {x}");
// 创建Math对象的实例
var math = new Math();
// 实例化成员
math.Value = 30;
Console.WriteLine($"Value field of math variable contains {math.Value}");
Console.WriteLine($"Square of 30 is {math.GetSquare()}");
}
}
重载就是在同一个类中,方法名相同,参数列表不同,返回类型可以不相同。参数列表不同包括:参数的个数不同,参数类型不同等。以下就是返回类型可以不相同,但是他们的参数列表必须不同
public void Method2(int i, string j)
{
}
public string Method2()
{
return null;
}
public string Method2(int i)
{
return null;
}
重写就是子类重写父类的方法,在调用的时候,子类的方法会覆盖父类的方法,也就是会调用子类的方法。在父类中的方法必须有修饰符virtual或是abstract,而在子类的方法中必须指明override。在重写中,子类继承父类,不同的实例化方式会调用不同的方法。
class Child:Parent
{
public override void ParentMethod()
{
Console.WriteLine("this is child");
}
}
class Parent
{
public virtual void ParentMethod()
{
Console.WriteLine("this is parent");
}
}
调用方法时,变量名不需要添加到调用中。然而,如果有如下的方法签名,用于移动矩形
public void MoveAndResize(int x,int y,int width, int height)
用下面的代码片段调用它,就不能从调用中看出使用了什么数字,这些数字用于哪里:
r.MoveAndResize(30,40,20,40);
可以改变调用,明确数字的含义
r.MoveAndResize(x:30,Y:40,width:20,height:40);
任何方法都可以使用命名的参数调用。只需要编写变量名,后跟一个冒号和所传递的值。编译器会去掉变
量名,创建一个方法调用,就像没有变量名一样。还可以用这种方式更改变量的顺序,编译器会重新安排,获得正确的顺序。
参数也可以是可选的。必须为可选参数提供默认值。可选参数还必须是方法定义的最后的参数:
public void TestMethod(int notoptionalNumber, int optionalNumber = 42)
{
Console.WriteLine(optionalNumber + notOptionalNumber);
}
这个方法可以使用一个或两个参数调用。传递一个参数,编译器就修改方法调用,给第二个参数传递42。
TestMethod(11);
TestMethod(11,22);
使用可选参数,可以定义数量可变的参数。然而,还有另一种语法允许传递数量可变的参数——这个
语法没有版本控制问题声明数组类型的参数(示例代码使用一个imt数组),添加params关键字,就可以使用任意数量的int参数调用该方法。
public void AnyNumberofArguments(params int] data)
声明基本构造函数的语法就是声明一个与包含的类同名的方法,但该方法没有返回类型:
public class MyClass
{
public MyClass()
{
}
}
一般情况下,如果没有提供任何构造函数,编译器会在后台生成一个默认的构造函数。这是一个非常基本的构造函数,它只能把所有的成员字段初始化为标准的默认值(例如,引用类型为空引用,数值数据类型为0,bool为false)。这通常就足
够了,否则就需要编写自己的构造函数。
无参数的静态构造函数,只执行一次。而实例构造函数在每次创建实例都会执行。
编写静态构造函数的一个原因是,类有一些静态字段或属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性。
public class MyClass
{
static MyClass()
{
}
}
如果构造函数的实现由一个表达式组成,那么构造函数可以通过一个表达式体来实现
public class Singleton
{
private static Singleton s_instance;
private int _state;
private Singleton(int state) =>_state= state;
public static Singleton Instance => s_instance ?? new Singleton(42);
}
由于一个类中的不同构造函数包含一些共同的代码,如初始化了相同的字段,最好把所有代码放在一个地方。
class Car
{
private string description;
private unit nWheels;
public Car(string description,unit nWheels)
{
this.description = description;
this.nWheels = nWheels;
}
public Car(string description) : this(description,4)
{
}
}
static 声明静态类,静态类访问不需要实例化。
静态类、静态方法、静态字段,直接使用类名.(点)
非静态类、非静态方法、非静态字段,需要实例化。
语法:类名 名称(类对象) = new 类名();
使用:类对象. (点)非静态方法或非静态字段
静态的从程序一启动就会一直占用内存,而非静态的只在使用后(实例化)后才会占用内存.但是每实例化个一个对象时又会另外占用内存. 举个例子,比如说一个数据库的连接字段(STRING).因为要经常使用到它,这时我们可以用STATIC.但是如果这时用非静态的话那就不合算了,因为每次调用到它时,又实例化一次.这样相比来说占用内存就比较大了.不划算. 像一个登录后台的方法,你只在登陆时候调用一次,就没有必要做成静态的了.那样一直驻存在内存中.在大型项目中,你如果都使用静态的那得要多少内存去支撑.
结构是值类型:
存储在栈中或存储为内联(如果它们是存储在堆中的另一个对象的一部分),其生存期的限制与简单的数据类型一样。
public struct Dimensions
{
public double Length { get; }
public double Width { get; }
public Dimensions(double length, double width)
{
Length = length;
Width = width;
}
public double Diagonal => Math.Sqrt(Length * Length + Width * Width);
}
Dimensions point = new Dimensions();
point.Length = 3;
point.Width = 6;
//可修改为如下方式
Dimensions point;
point.Length = 3;
point.Width = 6;
如果是类,则会产生一个编译错误,因为point包含一个未初始化的引用—不指向任何地方的一个地址,所有不能给其字段设置值。但对于结构,变量声明实际上是为整个结构在栈中分配空间,
所以就可以为它赋值了。但下面的代码会产生一个编译错误,编译器会抱怨用户使用了未初始化的变量:
Dimensions point;
Double D = point.Length;
需调用new运算符,或者给所有字段赋值,结构才完全初始化。但如果结构定义为类的成员字段,在初始化包含的对象时,该结构会自动初始化为0;
readonly修饰符可以应用于结构,因此编译器保证结构体的不变性。使用C#7.2时,可以声明前面定义的类型Dimensions为readonly,因为它只包含一个修改其成员的构造函数。属性只包含一个get访问器,因此不可能进行更改
public readonly struct Dimensions
{
public double Length { get; }
public double Width { get; }
public Dimensions(double length, double width)
{
Length = length;
Width = width;
}
public double Diagonal => Math.Sqrt(Length * Length + Width * Width);
}
对于readonly修饰符,如果在创建对象后类型更改了字段或属性,编译器就会报错。使用这个修饰符,编
译器可以生成优化的代码,使其在传递结构体时不会复制结构的内容;相反,编译器使用引用,因为它永远不
会改变。
为结构定义构造函数的方式与为类定义构造函数的方式相同前面说过,默认构造函数把数值字段都初始化为0,且总是隐式地给出,即使提供了其他带参数的构造函数,也是如此。不能为结构创建定制的默认构造函数。
结构并不总是放在堆栈上。它们也可以放在堆上。可以为对象分配一个结构体,这会在堆中创建一个对象。
如下面的代码片段所示。可以添加属性、值字段、引用类型和方法——就像其他结构一样
ref struct ValueTypeOnly
{
public ValueTypeOnly(int y)
{
Y = y;
X = 0;
}
public int X;
public int Y { get; }
public void AMethod()
{
Console.WriteLine($"x: {X}, y: {Y}");
}
}
这种类型不能执行的操作是将它分配给对象一例如,调用Object基类的方法(如ToString)。这将导致装
箱,并创建一个引用类型,这种类型是不允许这种操作的
正面:为结构分配内存速度非常快,在结构超出了作用域被删除时速度也很快。
负面:把结构作为参数来传递或者把结构赋予另一个结构(如A=B),结构的所有内容就被复制,而对于类,只复制引用。这样会有性能损失,根据结构大小,性能损失也不同。(可用ref避免)
C#默认都是值传递的,就是复制变量的一个副本传递给方法,所以在退出方法后,对变量的修改无效。
但是要注意,当传递是引用类型时,因为引用类型是一个引用的地址,所以修改引用地址指向的对象时,一样会修改对象的值,这种现象不能当做引用传递
class Demo
{
static void Main(string[] args)
{
int x = 8;
Fo(x);
Console.WriteLine("x={0}", x);
}
static void Fo(int p)
{
p = p + 1;
Console.WriteLine("p={0}", p);
}
}
程序运行结果为:p=9,x=8;即X的值不会受P影响,给P赋一个新值并不会改变X的内容,因为P和X存在于内存中不同的位置。
引用传递是对象本身传递给方法,当在方法中对对象做修改时,退出方法后修改是有效的,在C#中引用传递需要在参数类型前加关键字 ref ,但是ref 的参数变量在使用前必须被初使化,可有时通过引用传递的变量初值是没意义的,这容易产生混淆,这时在C#中有另一关键字 out ,out 与 ref 关键字使用时效果一样,但是out关键字对变量可以不初使化,但是out关键字的变量在方法中必须给分配一个值,否则编译会不通过
class Demo
{
static void Main(string[] args)
{
int x = 8;
Fo(x);
Console.WriteLine("x={0}", x);
}
static void Fo(ref int p)
{
p = p + 1;
Console.WriteLine("p={0}", p);
}
}
程序运行结果为:p=9,x=9;
当使用rel关键字时,表示是用引用的方式传递参数。实参和形参引用的都是同一个对象,改变其中一个的引用值,另一个也会改变。
ref 关键字使参数按引用传递。其效果是,当控制权传递回调用方法时,在方法中对参数所做的任何更改都将反映在该变量中。若要使用 ref 参数,则方法定义和调用方法都必须显式使用 ref 关键字。
out 关键字会导致参数通过引用来传递。这与 ref 关键字类似,不同之处在于 ref 要求变量必须在传递之前进行初始化。若要使用 out 参数,方法定义和调用方法都必须显式使用 out 关键字。
out修饰符允许返回参数指定的值。in修饰符保证发送到方法中的数据不会更改(在传递值类型时)。
下面定义一个简单的可变结构体,名称为AValueType,再定义一个公共可变字段
struct AValueType
{
public int Data;
}
class Program
{
static void Main(string[] args)
{
AValueType vt = new AValueType { Data = 42 };
CantChange(vt);
int dt = 42;
CantChange(dt);
}
static void CantChange(in AValueType a)
{
Console.WriteLine(a.Data);
}
static void CantChange(in int x)
{
// x = 43; // 只读
}
}
现在,使用i修饰符定义一个方法时,变量就不能更改了。试图更改可变字段Data,编译器会抱忽不能为只读变量的成员分配值,因为该变量是只读的。in修饰符使参数设置为只读变量
引用类型(类)的变量可以为空,而值类型(结构)的变量不能。可空类型是可以为空的值类型。可空类型只需要在类型的后面添加“?”(它必须是结构)。与基本结构相比,值类型唯一的开销是一个可以确定它是否为空的布尔成员。
在下面的代码片段中,x1是一个普通的it,x2是一个可以为空的int。因为x2是可以为空的int,所以可以把null分配给x2:
int?x2=null;
使用合并操作符??,可空类型可以使用较短的语法。如果x3是null,则用变量x6给它设置-1,否则提取x3的值:
intx6=x3??-1;
注意:
对于可空类型,可以使用能用于基本类型的所有可用操作符,例如,可用于int?的+、一、*、/等。
枚举是一个值类型,包含一组命名的常量。枚举类型用enum关键字定义
public enum Color : short
{
Red = 1,
Green = 2,
Blue = 3
}
Enum.GetNames方法返回一个包含所有枚举名的字符串数组:
为了获得枚举的所有值,可以使用方法Enum.GetValues。Enum.GetValues返回枚举值的一个数组。为了获
得整数值,需要把它转换为枚举的底层类型,为此应使用foreach语句:
Color red;
if (Enum.TryParse<Color>("Red", out red))
{
Console.WriteLine($"successfully parsed {red}");
}
string redtext = Enum.GetName(typeof(Color), red);
Console.WriteLine(redtext);
foreach (var day in Enum.GetNames(typeof(Color)))
{
Console.WriteLine(day);
}
foreach (short val in Enum.GetValues(typeof(Color)))
{
Console.WriteLine(val);
}
foreach (var item in Enum.GetValues(typeof(Color)))
{
Console.WriteLine(item);
}
partial关键字允许把类、结构、方法或接口放在多个文件中。一般情况下,某种类型的代码生成器生成了个类的某部分,所以把类放在多个文件中是有益的。假定要给类添加一些从工具中自动生成的内容。如果重
新运行该工具,前面所做的修改就会丢失。partial关键字有助于把类分开放在两个文件中,而对不由代码生成
器定义的文件进行修改partial关键字的用法是:把partial放在class、stmuct或interface关键字的前面。类驻留在两个不同的源文件SampleClassAutogenerated.cs和SampleClass.cs中:
SampleClass.cs:
//SampleClassAutogenerated.cs
partial class SampleClass
public void Methodone(){}
//Sampleclass.cs
partial class SampleClass
public void MethodTwo(){}
namespace ExtensionMethods
{
namespace Foo
{
public static class StringExtensions
{
public static int GetWordCount(this string s) =>
s.Split().Length;
}
}
namespace Bar
{
public static class StringExtensions2
{
public static int GetWordCount(this string s) =>
s.Split().Length;
}
}
class Program
{
static void Main()
{
string fox = "the quick brown fox jumped over the lazy dogs down 9876543210 times";
int wordCount = fox.GetWordCount();
Console.WriteLine($"{wordCount} words");
Console.ReadLine();
}
}
}
所有的NET类最终都派生自System.Object。
Object类定义的许多公有的和受保护的成员方法。这些方法可用于自己定义的所有其他类中。
ToString():是获取对象的字符串表示的一种便捷方式。当只需要快速获取对象的内容,以进行调
试时,就可以使用这个方法。
GetHashCod()方法:如果希望把类用作字典的一个键,就需要重写GetHashCod()方法。
Equals()和ReferenceEquals():注意有3个用于比较对象相等性的不同方法,这说
明.NETFramework在比较相等性方面有相当复杂的模式。这3个方法和比较运算符“=”在使用方式
上有微妙的区别。而且,在重写带一个参数的虚Equals0方法时也有一些限制,因为System.Collections
名称空间中的一些基类要调用该方法,并希望它以特定的方式执行。
Finaliz():如果对象拥有对非托管资源的引用,则在该对象被删除时,就需要删除这些引用,此时一般要重写
Finalize()。垃圾收集器不能直接删除这些对非托管资源的引用,因为它只负责托管的资源,于是它只能
依赖用户提供的Finalize().
GetType():这个方法返回从System.Type派生的类的一个实例,因此可以提供对象成员所属类的更
多信息,包括基本类型、方法、属性等。System.Type还提供了.NET的反射技术的入口点。
MembenwiseClone():复制对象,并返回对副本的一个引用。注意,得到的副本是一个浅表复制,即它复制了类中的所有值类型。如果类包含内嵌的引用,就只复制引用,而不复制引用的对象。这个方法是受保护的,所以不能用于复制外部的
对象。该方法不是虚方法,所以不能重写它的实现代码