本文介绍C#中类的创建和应用,其中,类的成员类型包括属性、方法、构造函数和析构函数、索引器等;然后介绍了静态类和静态方法,以及扩展方法的应用;最后,介绍了空值、var关键字和匿名类型、使用Type对象获取类型信息、Lambda表达式和结构。
前面的示例中已经创建了C1类,下面继续修改C1类的代码(/cfx/demo/C1.cs)。
using System; namespace cfx.demo { public class C1 { // public string Name = ""; // public void SayHello() { Console.WriteLine("Hello,{0}.", Name); } // public uint Age { get; set; } } }
代码中,定义了C1类的三个成员,分别是:
代码中的public表示成员的访问级别,C#中的访问级别包括:
此外,这三个成员都是实例成员,可以使用new关键字创建实例,然后由实例调用。下面的代码,在Program.cs中测试C1类,代码会创建C1类的实例,并赋值给obj1对象,然后通过obj1对象调用实例成员。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { C1 obj1 = new C1(); obj1.Name = "Tom"; obj1.Age = 25; obj1.SayHello(); // Hello,Tom. Console.WriteLine(obj1.Age); // 25 } } }
从代码应用的形式上看,字段和属性似乎区别并不大,但需要注意,字段是变量(对象)定义的,只能进行简单的数据读写操作,而属性则可以通过get和set语句块进行更多的读、写控制。
下面的代码会修改C1类中Age属性的定义。
using System; namespace cfx.demo { public class C1 { // public string Name = ""; // public void SayHello() { Console.WriteLine("Hello,{0}.", Name); } // private uint myAge=0; public uint Age { get { return myAge; } set { if (value > 150) throw new Exception("年龄一般不会大于150岁"); else myAge = value; } } } }
代码中,新增了私有(private)变量myAge保存年龄的属性值,默认值为0;然后,在get块中直接返回myAge变量的数据,即Age属性的值;set块中,使用value关键字读取设置的属性值,这里,当设置的属性值大于150时会抛出一个异常(Exception)对象,小于150时将value的值保存到myAge变量中;请注意,Age属性的数据类型定义为uint,只能处理大于等于0的整数,所以,当赋值为小于0的数据时无法通过编译,如果设置为有符号的数值类型,则在set语句块中还应处理属性值小于0的情况。
下面的代码,在Program.cs文件中测试Age属性的读、写操作。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { C1 obj1 = new C1(); obj1.Age = 25; Console.WriteLine(obj1.Age); // 25 obj1.Age = 200; // 抛出异常 } } }
代码执行结果如下图所示。
如果属性值只允许读取,即只读属性,可以只设置get语句块,不设置set语句块(可通过变量保存数据)或者将set语句块设置为private;只读属性一般在实例化时指定数据或通过计算返回数据;在类的外部只能读取,不能修改。
同样的,如果属性值只允许写入,不允许读取,即只写属性,则只设置set语句块,不设置get语句块;只写属性一般用于设置辅助数据。
方法定义的基本要素包括修饰符、方法名称、参数、返回值类型和方法体,格式如下:
<修饰符> <返回值类型> <方法名称>(<参数>) { <方法体> }
其中:
下面的代码,在Program.cs文件中进行测试。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { Console.WriteLine(Add(10, 99)); // 109 Console.WriteLine(Add(10)); // 10 } // static int Add(int x,int y = 0) { return x + y; } } }
代码中,除了Main()主方法,还定义了Add()方法,其功能是返回两个参数的和,其中,x参数定义为int类型,没有默认值,调用时必须指定数据;y参数同样定义为int类型,并指定的了默认值0。在Main()方法中,调用了两个Add()方法,第一次设置参数为10和99,返回结果为109;第二次只设置第一个参数x的值为10,y使用默认值,返回结果为10。
调用方法时,如果参数较多,也可以按参数名设置数据,而不必完全按定义的顺序指定数据,如下面的代码。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { Add(x: 10, z: 99); Add(y: 10, z: 99, x: 0); } // static void Add(int x, int y = 0, int z = 0) { Console.WriteLine("{0}+{1}+{2}={3}", x, y, z, x + y + z); } } }
Add()方法的参数按x、y、z定义,在Main()方法中调用Add()方法进,使用“参数名:值”的格式指定参数数据,可以看到,在Add()方法中正确地显示了参数对应的值和计算结果。
参数数组应用于参数类型相同,但数量未定的情况,调用方法时可以设置0个或多个参数,如下面的代码。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { Console.WriteLine(Add()); // 0 Console.WriteLine(Add(1)); // 1 Console.WriteLine(Add(1,2,3)); // 6 } // static int Add(params int[] args) { int sum = 0; for (int i = 0; i < args.Length; i++) sum += args[i]; return sum; } } }
参数数组在方法体中作为数组(Array)对象调用,本例的Add()方法中,使用for循环语句计算了所有参数的和并返回。Main()方法中调用了三次Add()方法,分别设置了0、1、3个参数。
重载(overload)是指具有相同名称的多个方法版本,但各个版本的参数设置不同,如下面的代码。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { Console.WriteLine(Add(10, 99)); Console.WriteLine(Add(1.1, 99)); } // static int Add(int x,int y) { Console.WriteLine("int Add(int,int)"); return x + y; } // static double Add(double x, double y) { Console.WriteLine("double Add(double,double)"); return x + y; } } }
代码中定义了两个Add()方法,第一个版本的参数和返回值都是int类型,第二个版本的参数和返回值都是double类型;代码执行结果如下图所示。
下面的代码演示了按引用传递参数。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { int a = 10, b = 99; Console.WriteLine("a={0}, b={1}", a, b); Swap(ref a, ref b); Console.WriteLine("a={0}, b={1}", a, b); } // static void Swap(ref int x,ref int y) { int tmp = x; x = y; y = tmp; } } }
代码中定义了Swap()方法,其功能是交换两个参数的值,这里使用ref关键字将参数定义为按引用传递,只有这样,在方法中才能真正修改传入的变量数据,否则在方法中操作的只是传入变量的副本,无法真正修改外部变量的值。本例执行结果如下图所示。
需要注意的是,类类型默认是按引用传递,方法中可以修改实例的数据,但不能修改对象指向的实例,如果需要修改外部对象的实例,应在方法参数中使用ref关键字定义。结构等值类型则是按值传递,需要在方法中修改外部变量的值时必须使用ref关键字定义为按引用传递参数。
如果一个方法需要返回的数据不止一个,可以使用输出参数实现,方法定义中,输出参数使用out关键字定义,如下面的代码。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { object obj = "abc"; int result; if (ToInt(obj, out result)) Console.WriteLine("成功转换为整数{0}", result); else Console.WriteLine("无法转换为整数"); } // static bool ToInt(object obj ,out int result) { result = 0; return obj != null && int.TryParse(obj.ToString(), out result); } } }
代码中的ToInt()方法第二个参数使用输出参数,用于输出转换的整数结果,而方法本身会返回布尔类型(bool)类型,判断是否转换成功;方法中调用的int.TryParse()方法的第二个参数也定义为输出参数,这里直接使用ToInt()方法的result参数输出。
Main()方法中,首先定义obj对象保存需要转换的数据,这里可以修改obj对象的值来观察执行结果。变量result用于保存ToInt()方法输出的转换结果数据,如果obj对象的值可以转换为int类型,则显示其转换结果,否则显示"无法转换为整数"。
构造函数会在创建类的实例时调用,在类中使用与类同名的方法创建构造函数,如下面的代码(/cfx/demo/C1.cs)。
using System; namespace cfx.demo { public class C1 { public C1() { } public C1(string name) { Name = name; } // public string Name = ""; // public void SayHello() { Console.WriteLine("Hello,{0}.", Name); } // public uint Age { get; set; } } }
代码中,在C1类中创建了两个构造函数,即两个重载版本的构造函数。需要注意,当类中没有定义构造函数时,会自动包含一个没有参数的构造函数;如果创建了有参数的构造函数,则不再默认包含无参数的构造函数,如果需要则必须创建一个。
下面的代码,在Program.cs文件中测试C1类的构造函数。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { C1 obj1 = new C1(); C1 obj2 = new C1("Tom"); obj2.SayHello(); } } }
执行代码会显示“Hello,Tom.”。
此外,构造函数还可以调用其它的构造函数,并且,构造函数还可以对只读属性进行赋值,如下面的代码(/cfs/demo/C1.cs)。
using System; namespace cfx.demo { public class C1 { public C1():this("Noname") { } public C1(string name) { Name = name; } // public string Name { get; private set; } // public void SayHello() { Console.WriteLine("Hello,{0}.", Name); } } }
本例,将Name属性定义为只读属性,C1()构造函数调用了C1(string)构造函数,并指定默认姓名(Name属性)为“Noname”;C1(string)构造函数的参数需要指定Name属性值。下面的代码,在Program.cs文件中进行测试。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { C1 obj1 = new C1(); C1 obj2 = new C1("Tom"); obj1.SayHello(); // Hello,Noname. obj2.SayHello(); // Hello,Tom. } } }
执行代码会显示"Hello,Noname."和"Hello,Tom."。
实际中,还可以根据需要修改构造函数定义的修饰符,如使用private、protected、internal、static等关键字。
虽然构造函数可以通过重载、可选参数等创建多个版本,但是,如果属性过多,构造函数的定义也会显得麻烦,此时,属性可以设置为常用的默认值,然后,在创建对象时使用初始化器设置所需要的属性值,即在创建实例后使用一对花括号定义属性值,如下面的代码。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { C1 obj1 = new C1() { Name="Tom" }; // obj1.SayHello(); } } }
此外,设置多个属性数据时使用逗号分隔。
析构函数可以在对象释放时自动调用,其命名格式为“~<类名>”,不需要修饰符和返回值类型,如下面的代码(/cfs/demo/C1.cs)。
using System; namespace cfx.demo { public class C1 { public C1():this("Noname") { } public C1(string name) { Name = name; } // public string Name { get; private set; } // public void SayHello() { Console.WriteLine("Hello,{0}.", Name); } // ~C1() { Console.WriteLine("{0}已穿越……", Name); } } }
下面的代码,在Program.cs文件进行测试。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { C1 obj1 = new C1(); C1 obj2 = new C1("Tom"); } } }
本例创建了两个C1类型的对象,然后什么也没做。对象不再使用时会自动释放内存,此时会自动调用C1类的析构函数,执行效果如下图所示。
另一种能够自动调用对象清理的机制是使用using语句结构操作对象,此时的对象类型应实现了IDisposable接口,在对象释放时,无论代码是否正确执行,都会调用Dispose()方法,如下面的代码(/cfx/demo/C1.cs)。
using System; namespace cfx.demo { public class C1 : IDisposable { public C1():this("Noname") { } public C1(string name) { Name = name; } // public string Name { get; set; } // public void SayHello() { Console.WriteLine("Hello,{0}.", Name); } public void Dispose() { Console.WriteLine("对象已清理"); } // ~C1() { Console.WriteLine("{0}已穿越……", Name); } } }
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { using (C1 obj1 = new C1("Tom")) { obj1.SayHello(); } } } }
使用 using语句结构可以保证无论结构中的代码是否正确执行,都可以在对象释放前对资源进行必要的清理,如关闭打开的文件、关闭打开的数据库连接等。
索引器可以让对象使用一对中括号按索引访问数据,如数组(Array)、列表(List)、字典(Dictionary)等对象。在自定义类中,可以使用“this[<索引类型> <索引变量>]”格式创建索引器,如下面的代码(/cfx/demo/)。
using System.Collections.Generic; // 斐波那契数列(Fibonacci Sequence) namespace cfx.demo { public class Fib { private List<long> mySeq; // public Fib() { mySeq = new List<long>(); mySeq.Add(1); mySeq.Add(1); } // 读取数列的数据,n是大于等于0的整数 public long this[int n] { get { // 转换为mySeq的索引 if (n < mySeq.Count) { return mySeq[n]; } else { // 根据读取需要生成数列中的数值 for (int i = mySeq.Count; i <= n; i++) mySeq.Add(mySeq[i - 2] + mySeq[i - 1]); // return mySeq[n]; } } } } }
Fib类的功能是实现斐波那契数列(Fibonacci Sequence),其中,索引器使用大于等于0的值,即第一个读取的值索引是0,第二个索引值是1,以此类推。索引器中同样使用get块返回读取的数据,本例定义为只读索引器,如果定义索引器的写入操作,应使用set块,与属性的定义类似。
下面的代码,在Program.cs文件中显示斐波那契数列的前10个数值。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { Fib fib = new Fib(); for(int i =0; i < 10; i++) { Console.WriteLine(fib[i]); } } } }
代码执行效果如下图所示。
定义索引器时,索引类型除了使用整数,还可以使用字符串等类型,可以根据数据结构的实际需要设置。
前面创建的类成员需要使用类的实例(对象)调用,称为类的实例成员,每一个实例都会有独立的副本。工作中,有些代码只需要一个副本,此时可以定义为静态成员,如下面的代码(/cfx/demo/ObjectFactory.cs)。
using System; namespace cfx.demo { public class ObjectFactory { // 创建对象计数 public static int Counter; // 静态构造函数 static ObjectFactory() { Counter = 0; } // public ObjectFactory() { Counter = Counter + 1; Console.WriteLine("创建第{0}个对象...", Counter); } // } }
代码中创建了ObjectFactory类,其中,Counter为静态字段,用于保存创建对象的数量;然后定义了静态构造函数,其中将Counter字段的值设置为0。最后是实例化的构造函数,每一次创建实例时Counter字段都会加1,并显示创建第几个对象。下面的代码,在Program.cs文件中测试ObjectFactory类。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { ObjectFactory f1 = new ObjectFactory(); ObjectFactory f2 = new ObjectFactory(); ObjectFactory f3 = new ObjectFactory(); } } }
代码中,创建了三个ObjectFactory对象,执行结果如下图所示。
类中的静态成员可以创建构造函数、字段、属性、方法等要素,这些静态成员由类名直接调用,并可以在实例成员中调用,但反过来,实例成员不能在静态成员中调用,因为实例成员只属于某一个具体的实例。
除了定义类的静态成员,类也可以定义为静态的。静态类一般用于封装代码,如System命名空间中的Console类、Math类等。静态类和静态方法还可以用于扩展现有类的实例方法,下面的代码(/cfx/Str.cs)演示了相关应用。
using System; using System.Text.RegularExpressions; namespace cfx { public static class Str { // 转换为int类型,默认值为0 public static int ToInt(this string s, int defVal = 0) { int result; if (int.TryParse(s, out result)) return result; else return defVal; } } }
代码中定义了Str静态类,其中,定义了ToInt()静态方法,请注意,第一个参数定义为string类型,并使用了this关键字,此时,ToInt()方法就定义为string类型的扩展方法,可以通过字符串对象直接调用。ToInt()方法的第二个参数指定默认值,当参数一的字符串能够正确转换为int类型时返回转换结果,否则返回默认值。
下面的代码,在Program.cs文件中测试Str.ToInt()方法。
using System; using cfx; namespace csfx_demo { class Program { static void Main(string[] args) { string s = "123"; Console.WriteLine(s.ToInt()); // 123 Console.WriteLine("abc".ToInt()); // 0 Console.WriteLine(Str.ToInt("99")); // 99 } } }
代码中,第一次使用字符串对象s调用ToInt()方法;第二次使用字符串字面量调用ToInt()方法;第三次使用类名和方法名调用Str.ToInt()方法。
当对象没有赋予类的实例时,其值为空值,使用null关键字表示。代码中,结构类型,如各种数值类型是不能为空值的,但可以在标准类型关键字后添加问号(?)创建对应的可空类型,如下面的代码。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { int? x = null; if (x == null) Console.WriteLine("变量为空"); else Console.WriteLine("x={0}", x); } } }
执行代码会显示“变量为空”,可以修改变量x的值来观察运行结果。
??运算符需要两个运算数,左侧为对象,当对象不是空值(null)时返回对象,否则返回运算符右侧表达式的值,如下面的代码。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { C1 obj1 = new C1(); C1 obj2 = obj1 ?? new C1("Tom"); obj2.SayHello(); } } }
执行代码会显示“Hello.Noname.”,如果将obj1设置为null,则会显示“Hello,Tom.”。
?运算符可以放在对象右侧,当对象不为空值时调用成员,否则不执行任何操作,如下面的代码。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { C1 obj1 = new C1(); obj1?.SayHello(); } } }
执行代码会显示“Hello,Noname.”,如果将obj1设置为null,则什么也不执行。
使用var关键字可以在声明变量或对象时不指定类型,变量或对象类型会根据赋值的数据而定;此外,对于一次性使用的数据结构,可以直接使用匿名类型定义,下面的代码演示了var关键字和匿名类型的应用。
using System; using cfx.demo; namespace csfx_demo { class Program { static void Main(string[] args) { var obj1 = new C1(); obj1?.SayHello(); // var obj2 = new { Name = "Tom", Age = 25 }; Console.WriteLine("Name:{0}, Age:{1}", obj2.Name, obj2.Age); } } }
执行结果如下图所示。
代码中,可以通过Type类获取变量、对象或字面量的类型信息。Type类为抽象类,不能直接实例化,但可以通过变量、对象和字面量的GetType()方法获取类型对象,也可以使用typeof运算符通过类型名称获取Type对象,然后读取相关的信息,如下面的代码。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { int x = 123; Type t1 = x.GetType(); Type t2 = typeof(string); // Console.WriteLine(t1.Name); // Int32 Console.WriteLine(t2.Name); // String } } }
代码中,获取了整数(int)和string类型的Type对象,并通过Name属性显示类型名称。处理类型时应注意,Type对象中显示的名称是.NET Framework中定义的类型名称。
Type对象中常用的属性有:
Type对象中常用的方法有:
关于接口、继承、枚举、委托与事件、泛型等内容会在后续文章中详细讨论。
Lambda表达式是一种简化方法定义的语法格式,可以定义匿名方法。定义Lambda表达式使用=>运算符,运算符左侧可以看作是方法的参数,右侧是方法的实现代码,右侧表达式的值即是方法的返回值。下面的代码演示了Lambada表达式的应用。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { int x = 10, y = 99; Console.WriteLine(Add(x, y)); } // static int Add(int x, int y) => x + y; } }
代码中,Add()方法的定义使用了Lambda表达式,省略了花括号定义的方法体;这里定义的Add()方法与下面的代码功能相同。
static int Add(int x, int y) { return x + y; }
结构使用struct关键字定义,其中,可以定义构造函数、变量、常量、方法、属性、索引器、事件、重载运算符等成员。定义结构的语法与类相似,如下面的代码。
using System; namespace csfx_demo { class Program { static void Main(string[] args) { Point pt = new Point(10, 99); Console.WriteLine("({0}, {1})", pt.X, pt.Y); } } // struct Point { public int X; public int Y; // public Point(int x,int y) { X = x; Y = y; } } }
代码中定义了Point结构,其中包括两个字段X和Y;此外,还定义了构造函数,其参数包括x和y。Main()方法中,首先创建Point结构类型变量pt,并指定字段数据分别为10和99,最后显示信息为“(10, 99)”。
实际应用中,类为引用类型,结构为值类型。此外,类还可以通过实现接口类型和继承等方式定义更加复杂的代码结构,后续文章中还会有详细讨论。