目录
概述
C# 作为一门功能强大且灵活的编程语言,广泛应用于各种软件开发领域。从基础语法到高级特性,掌握这些知识对于开发高效、健壮的应用程序至关重要。在本系列技术博客中,我们将通过一系列实际代码示例,深入探讨C#中的关键概念与应用。
静态与非静态变量
示例代码:
using System;
namespace Example01
{
class Program
{
class Class1
{
public static String staticStr = "Class";
public String notstaticStr = "Obj";
}
static void Main(string[] args)
{
// 静态变量通过类进行访问,该类所有实例的同一静态变量都是同一个值
Console.WriteLine("Class1's staticStr: {0}", Class1.staticStr);
Class1 tmpObj1 = new Class1();
tmpObj1.notstaticStr = "tmpObj1";
Class1 tmpObj2 = new Class1();
tmpObj2.notstaticStr = "tmpObj2";
// 非静态变量通过对象进行访问,不同对象的同一非静态变量可以有不同的值
Console.WriteLine("tmpObj1's notstaticStr: {0}", tmpObj1.notstaticStr);
Console.WriteLine("tmpObj2's notstaticStr: {0}", tmpObj2.notstaticStr);
Console.ReadLine();
}
}
}
运行结果:
Class1's staticStr: Class
tmpObj1's notstaticStr: tmpObj1
tmpObj2's notstaticStr: tmpObj2
解析:
在C#中,静态变量属于类本身,而非静态变量属于类的实例。静态变量在内存中只有一份,所有实例共享;而每个实例拥有自己的一份非静态变量。
常量与只读变量
示例代码:
测试类(Example02Lib):
using System;
namespace Example02Lib
{
public class Class1
{
public const String strConst = "Const";
public static readonly String strStaticReadonly = "StaticReadonly";
}
}
客户端代码(Example02):
using System;
using Example02Lib;
namespace Example02
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("strConst : {0}", Class1.strConst);
Console.WriteLine("strStaticReadonly : {0}", Class1.strStaticReadonly);
Console.ReadLine();
}
}
}
运行结果:
strConst : Const
strStaticReadonly : StaticReadonly
修改后的测试类:
namespace Example02Lib
{
public class Class1
{
public const String strConst = "Const Changed";
public static readonly String strStaticReadonly = "StaticReadonly Changed";
}
}
运行结果:
strConst : Const
strStaticReadonly : StaticReadonly Changed
解析:
-
const
(常量)在编译时被替换为其值,不能修改。如果修改常量的值,需要重新编译使用该常量的所有代码。 -
readonly
(只读变量)在运行时被赋值,可以在声明时或在构造函数中赋值,适用于需要在运行时确定值的场景。
平台调用(P/Invoke)
示例代码:
using System;
using System.Runtime.InteropServices;
namespace Example03
{
class Program
{
// 注意DllImport是一个Attribute Property,在System.Runtime.InteropServices命名空间中定义
[DllImport("User32.dll")]
public static extern int MessageBox(int Handle, string Message, string Caption, int Type);
static int Main()
{
string myString;
Console.Write("Enter your message: ");
myString = Console.ReadLine();
return MessageBox(0, myString, "My Message Box", 0);
}
}
}
运行结果:
解析:
通过P/Invoke,可以调用Windows API或其他非托管代码。在示例中,使用DllImport
特性引入User32.dll
中的MessageBox
函数,实现显示消息框的功能。
抽象类与继承
示例代码:
using System;
namespace Example04
{
#region 基类,抽象类
public abstract class BaseClass
{
// 抽象属性,同时具有get和set访问器表示继承类必须将该属性实现为可读写
public abstract String Attribute { get; set; }
// 抽象方法,传入一个字符串参数无返回值
public abstract void Function(String value);
// 抽象事件,类型为系统预定义的代理(delegate):EventHandler
public abstract event EventHandler Event;
// 抽象索引指示器,只具有get访问器表示继承类必须将该索引指示器实现为只读
public abstract Char this[int Index] { get; }
}
#endregion
#region 继承类
public class DeriveClass : BaseClass
{
private String attribute;
public override String Attribute
{
get { return attribute; }
set { attribute = value; }
}
public override void Function(String value)
{
attribute = value;
if (Event != null)
{
Event(this, new EventArgs());
}
}
public override event EventHandler Event;
public override Char this[int Index]
{
get { return attribute[Index]; }
}
}
#endregion
class Program
{
static void OnFunction(object sender, EventArgs e)
{
for (int i = 0; i < ((DeriveClass)sender).Attribute.Length; i++)
{
Console.WriteLine(((DeriveClass)sender)[i]);
}
}
static void Main(string[] args)
{
DeriveClass tmpObj = new DeriveClass();
tmpObj.Attribute = "1234567";
Console.WriteLine(tmpObj.Attribute);
// 将静态函数OnFunction与tmpObj对象的Event事件进行关联
tmpObj.Event += new EventHandler(OnFunction);
tmpObj.Function("7654321");
Console.ReadLine();
}
}
}
运行结果:
1234567
7
6
5
4
3
2
1
解析:
-
抽象类
BaseClass
定义了抽象属性、方法、事件和索引器,强制继承类实现这些成员。 -
DeriveClass
继承自BaseClass
,实现了所有抽象成员,并通过事件机制展示了如何在方法中触发事件。
访问修饰符
示例代码:
Lib01 的 Class1
:
using System;
namespace Example05Lib
{
public class Class1
{
internal String strInternal = null;
public String strPublic;
protected internal String strInternalProtected = null;
}
}
运行结果解析:
-
同一程序集内:
Example05Lib
中的Class2
类可以访问Class1
的strInternal
和strInternalProtected
成员。 -
不同程序集内:
Example05
项目的Class3
类无法访问Class1
的strInternal
成员,但可以访问strInternalProtected
成员(如果Class3
继承自Class1
)。
解析:
C#中的访问修饰符控制类成员的可见性:
-
public
:公开,任何地方都可访问。 -
internal
:仅在同一程序集内可访问。 -
protected internal
:在同一程序集内或通过继承可访问。 -
private
:仅在声明的类内部可访问。
密封类与方法
示例代码:
using System;
namespace Example06
{
class Program
{
class A
{
public virtual void F()
{
Console.WriteLine("A.F");
}
public virtual void G()
{
Console.WriteLine("A.G");
}
}
class B : A
{
public sealed override void F()
{
Console.WriteLine("B.F");
}
public override void G()
{
Console.WriteLine("B.G");
}
}
class C : B
{
// 无法重写 F 方法,因为在 B 中已被密封
public override void G()
{
Console.WriteLine("C.G");
}
}
static void Main(string[] args)
{
new A().F();
new A().G();
new B().F();
new B().G();
new C().F();
new C().G();
Console.ReadLine();
}
}
}
运行结果:
A.F
A.G
B.F
B.G
B.F
C.G
解析:
-
sealed
关键字用于阻止进一步的继承。在B
类中,方法F
被密封,意味着在C
类中无法重写F
方法。 -
虽然
B
类继承自A
并重写了F
和G
方法,但只有F
方法被密封,G
方法仍可在C
类中重写。
索引器的使用
示例代码:
using System;
namespace Example08
{
public class Point
{
private double x, y;
public Point(double X, double Y)
{
x = X;
y = Y;
}
// 重写ToString方法方便输出
public override string ToString()
{
return String.Format("X: {0} , Y: {1}", x, y);
}
}
public class Points
{
Point[] points;
public Points(Point[] Points)
{
points = Points;
}
public int PointNumber
{
get { return points.Length; }
}
// 实现索引访问器
public Point this[int Index]
{
get { return points[Index]; }
}
}
// 索引器的实质是含参属性,参数并不限于int
class WeatherOfWeek
{
public string this[int Index]
{
get
{
switch (Index)
{
case 0:
return "Today is cloudy!";
case 5:
return "Today is thundershower!";
default:
return "Today is fine!";
}
}
}
public string this[string Day]
{
get
{
string TodayWeather = null;
switch (Day)
{
case "Sunday":
TodayWeather = "Today is cloudy!";
break;
case "Friday":
TodayWeather = "Today is thundershower!";
break;
default:
TodayWeather = "Today is fine!";
break;
}
return TodayWeather;
}
}
}
class Program
{
static void Main(string[] args)
{
Point[] tmpPoints = new Point[10];
for (int i = 0; i < tmpPoints.Length; i++)
{
tmpPoints[i] = new Point(i, Math.Sin(i));
}
Points tmpObj = new Points(tmpPoints);
for (int i = 0; i < tmpObj.PointNumber; i++)
{
Console.WriteLine(tmpObj[i]);
}
string[] Week = new string[] { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Staurday" };
WeatherOfWeek tmpWeatherOfWeek = new WeatherOfWeek();
for (int i = 0; i < 6; i++)
{
Console.WriteLine(tmpWeatherOfWeek[i]);
}
foreach (string tmpDay in Week)
{
Console.WriteLine(tmpWeatherOfWeek[tmpDay]);
}
Console.ReadLine();
}
}
}
运行结果:
X: 0 , Y: 0
X: 1 , Y: 0.841470984807897
X: 2 , Y: 0.909297426825682
X: 3 , Y: 0.141120008059867
X: 4 , Y: -0.756802495307928
X: 5 , Y: -0.958924274663138
X: 6 , Y: -0.279415498198926
X: 7 , Y: 0.656986598718789
X: 8 , Y: 0.989358246623382
X: 9 , Y: 0.412118485241757
Today is cloudy!
Today is fine!
Today is fine!
Today is fine!
Today is fine!
Today is thundershower!
Today is cloudy!
Today is fine!
Today is fine!
Today is fine!
Today is fine!
Today is thundershower!
Today is fine!
解析:
-
索引器允许对象通过索引方式访问其内部数据结构。
-
Points
类通过整数索引器访问Point
数组中的元素。 -
WeatherOfWeek
类通过整数和字符串索引器分别返回不同的天气信息,展示了索引器参数的多样性。
操作符重载
示例代码:
using System;
namespace Example09
{
class BaseClass
{
// 基类设计者声明了一个PI的公共变量,方便进行运算
public static double PI = 3.1415;
}
class DervieClass : BaseClass
{
// 继承类发现该变量的值不能满足运算精度,于是可以通过new修饰符显式隐藏基类中的声明
public new static double PI = 3.1415926;
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(BaseClass.PI);
Console.WriteLine(DervieClass.PI);
Console.ReadLine();
}
}
}
运行结果:
3.1415
3.1415926
解析:
-
使用
new
关键字可以隐藏基类中的成员。在示例中,DervieClass
通过new
关键字重新定义了PI
变量,提供了更高的精度。
命名空间与别名
示例代码:
Lib01 的 Class1
:
using System;
namespace com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01
{
class Class1
{
public override string ToString()
{
return "com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01's Class1";
}
}
}
Lib02 的 Class1
:
using System;
namespace com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib02
{
class Class1
{
public override string ToString()
{
return "com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib02's Class1";
}
}
}
主单元(Program.cs):
using System;
// 使用别名指示符解决同名类型的冲突
// 在所有命名空间最外层定义,作用域为整个单元文件
using Lib01Class1 = com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01.Class1;
using Lib02Class2 = com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib02.Class1;
namespace Example19
{
namespace Test1
{
// Test1Class1在Test1命名空间内定义,作用域仅在Test1之内
using Test1Class1 = com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01.Class1;
class Class1
{
Lib01Class1 tmpObj1 = new Lib01Class1();
Lib02Class2 tmpObj2 = new Lib02Class2();
Test1Class1 tmpObj3 = new Test1Class1();
}
}
namespace Test2
{
using Test1Class2 = com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01.Class1;
class Program
{
static void Main(string[] args)
{
Lib01Class1 tmpObj1 = new Lib01Class1();
Lib02Class2 tmpObj2 = new Lib02Class2();
Test1Class2 tmpObj3 = new Test1Class2();
Console.WriteLine(tmpObj1);
Console.WriteLine(tmpObj2);
Console.WriteLine(tmpObj3);
Console.ReadLine();
}
}
}
}
运行结果:
com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01's Class1
com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib02's Class1
com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01's Class1
解析:
-
使用
using
别名可以解决不同命名空间中同名类型的冲突,提高代码的可读性和维护性。 -
别名的作用域取决于其声明的位置,最外层的别名作用于整个单元文件,内部命名空间的别名仅在其内部有效。
IDisposable 接口与资源管理
示例代码:
using System;
namespace Example20
{
class Program
{
class Class1 : IDisposable
{
// 析构函数,编译后变成 protected void Finalize(),GC会在回收对象前调用该方法
~Class1()
{
Dispose(false);
}
// 通过实现该接口,客户可以显式地释放对象,而不需要等待GC来释放资源
public void Dispose()
{
Dispose(true);
}
// 将释放非托管资源设计成一个虚函数,提供在继承类中释放基类的资源的能力
protected virtual void ReleaseUnmanageResources()
{
// Do something...
}
// 私有函数用以释放非托管资源
private void Dispose(bool disposing)
{
ReleaseUnmanageResources();
// 为true时表示是客户显式调用了释放函数,需通知GC不要再调用对象的Finalize方法
// 为false时表示是GC调用了对象的Finalize方法,无需额外操作
if (disposing)
{
GC.SuppressFinalize(this);
}
}
}
static void Main(string[] args)
{
// tmpObj1没有手工释放资源,就等着GC来慢慢释放它
Class1 tmpObj1 = new Class1();
// tmpObj2调用了Dispose方法,显式释放资源
Class1 tmpObj2 = new Class1();
tmpObj2.Dispose();
Console.ReadLine();
}
}
}
解析:
-
IDisposable
接口用于释放非托管资源,避免资源泄漏。 -
显式调用
Dispose
方法可以及时释放资源,而不是依赖垃圾回收器。 -
析构函数(最终器)在对象被垃圾回收前调用,但其执行时间不可预测。
字符串与StringBuilder的性能对比
示例代码:
using System;
using System.Text;
namespace Example22
{
class Program
{
static void Main(string[] args)
{
const int cycle = 10000;
long vTickCount = Environment.TickCount;
String str = null;
for (int i = 0; i < cycle; i++)
str += i.ToString();
Console.WriteLine("String: {0} MSEL", Environment.TickCount - vTickCount);
vTickCount = Environment.TickCount;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cycle; i++)
sb.Append(i);
Console.WriteLine("StringBuilder: {0} MSEL", Environment.TickCount - vTickCount);
string tmpStr1 = "A";
string tmpStr2 = tmpStr1;
Console.WriteLine(tmpStr1);
Console.WriteLine(tmpStr2);
// 注意后面的输出结果,tmpStr1的值改变并未影响到tmpStr2的值
tmpStr1 = "B";
Console.WriteLine(tmpStr1);
Console.WriteLine(tmpStr2);
Console.ReadLine();
}
}
}
运行结果:
String: 375 MSEL
StringBuilder: 16 MSEL
A
A
B
A
解析:
-
使用字符串拼接(
str +=
)在大量循环中性能较差,因为字符串是不可变的,每次拼接都会创建新的字符串实例。 -
StringBuilder
适用于需要频繁修改字符串的场景,性能显著优于直接使用字符串拼接。 -
字符串是引用类型,但由于其不可变性,改变一个引用不会影响到其他引用。
隐式与显式转换操作符
示例代码:
using System;
namespace Example23
{
class Program
{
// 灵感来源于大话西游经典台词“神仙?妖怪?”
class Immortal
{
public string name;
public Immortal(string Name)
{
name = Name;
}
// 隐式转换
public static implicit operator Monster(Immortal value)
{
return new Monster(value.name + ":神仙变妖怪?偷偷下凡即可。。。");
}
}
class Monster
{
public string name;
public Monster(string Name)
{
name = Name;
}
// 显式转换
public static explicit operator Immortal(Monster value)
{
return new Immortal(value.name + ":妖怪想当神仙?再去修炼五百年!");
}
}
static void Main(string[] args)
{
Immortal tmpImmortal = new Immortal("紫霞仙子");
// 隐式转换
Monster tmpObj1 = tmpImmortal;
Console.WriteLine(tmpObj1.name);
Monster tmpMonster = new Monster("孙悟空");
// 显式转换
Immortal tmpObj2 = (Immortal)tmpMonster;
Console.WriteLine(tmpObj2.name);
Console.ReadLine();
}
}
}
运行结果:
紫霞仙子:神仙变妖怪?偷偷下凡即可。。。
孙悟空:妖怪想当神仙?再去修炼五百年!
解析:
-
隐式转换(
implicit
)允许在不显式转换的情况下进行类型转换,需确保转换不会丢失数据或产生异常。 -
显式转换(
explicit
)需要在代码中明确指出转换,适用于可能导致数据丢失或异常的情况。 -
自定义转换操作符提高了类的灵活性和可用性。
可变参数方法
示例代码:
using System;
namespace ConsoleApplication1
{
class App
{
// 第一个参数必须是整型,后面的参数个数是可变的
// 由于定义的是object数组,所有的数据类型都可以作为参数传入
public static void UseParams(int id, params object[] list)
{
Console.WriteLine(id);
for (int i = 0; i < list.Length; i++)
{
Console.WriteLine(list[i]);
}
}
static void Main()
{
// 可变参数部分传入了三个参数,都是字符串类型
UseParams(1, "a", "b", "c");
// 可变参数部分传入了四个参数,分别为字符串、整数、浮点数和双精度浮点数数组
UseParams(2, "d", 100, 33.33, new double[] { 1.1, 2.2 });
Console.ReadLine();
}
}
}
运行结果:
1
a
b
c
2
d
100
33.33
System.Double[]
解析:
-
params
关键字允许方法接受可变数量的参数,适用于参数数量不确定的场景。 -
可变参数必须作为方法的最后一个参数,并且只能有一个
params
参数。 -
参数类型通常为数组,但也可以是其他类型,如
object[]
,支持传入不同类型的参数。
反射与动态实例化
示例代码:
Lib01 的 Class1
:
using System;
namespace Example25Lib
{
public class Class1
{
private string name;
private int age;
public Class1(string Name, int Age)
{
name = Name;
age = Age;
}
public void ChangeName(string NewName)
{
name = NewName;
}
public void ChangeAge(int NewAge)
{
age = NewAge;
}
public override string ToString()
{
return string.Format("Name: {0}, Age: {1}", name, age);
}
}
}
主单元(Program.cs):
using System;
using System.Reflection;
namespace Example25
{
class Program
{
static void Main(string[] args)
{
// 加载程序集
Assembly tmpAss = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "Example25Lib.dll");
// 遍历程序集内所有的类型,并实例化
Type[] tmpTypes = tmpAss.GetTypes();
foreach (Type tmpType in tmpTypes)
{
// 获取第一个类型的构造函数信息
ConstructorInfo[] tmpConsInfos = tmpType.GetConstructors();
foreach (ConstructorInfo tmpConsInfo in tmpConsInfos)
{
// 为构造函数生成调用的参数集合
ParameterInfo[] tmpParamInfos = tmpConsInfo.GetParameters();
object[] tmpParams = new object[tmpParamInfos.Length];
for (int i = 0; i < tmpParamInfos.Length; i++)
{
tmpParams[i] = tmpAss.CreateInstance(tmpParamInfos[i].ParameterType.FullName);
if (tmpParamInfos[i].ParameterType.FullName == "System.String")
{
tmpParams[i] = "Clark";
}
if (tmpParamInfos[i].ParameterType.FullName == "System.Int32")
{
tmpParams[i] = 27;
}
}
// 实例化对象
object tmpObj = tmpConsInfo.Invoke(tmpParams);
Console.WriteLine(tmpObj);
// 获取所有方法并执行
foreach (MethodInfo tmpMethod in tmpType.GetMethods())
{
// 为方法的调用创建参数集合
tmpParamInfos = tmpMethod.GetParameters();
tmpParams = new object[tmpParamInfos.Length];
for (int i = 0; i < tmpParamInfos.Length; i++)
{
tmpParams[i] = tmpAss.CreateInstance(tmpParamInfos[i].ParameterType.FullName);
if (tmpParamInfos[i].ParameterType.FullName == "System.String")
{
tmpParams[i] = "Clark Zheng";
}
if (tmpParamInfos[i].ParameterType.FullName == "System.Int32")
{
tmpParams[i] = 27;
}
}
tmpMethod.Invoke(tmpObj, tmpParams);
}
// 调用完方法后再次打印对象,比较结果
Console.WriteLine(tmpObj);
}
}
Console.ReadLine();
}
}
}
运行结果:
Name: Clark, Age: 0
Name: Clark Zheng, Age: 27
解析:
-
反射允许在运行时加载程序集、获取类型信息并动态创建对象实例。
-
示例中,通过反射加载
Example25Lib.dll
,获取Class1
类型,动态调用构造函数和方法,实现对象的实例化与操作。 -
反射强大但性能较低,应在必要时使用,避免在性能敏感的场景中过度依赖。
结语
通过以上多个示例,我们深入探讨了C#中的重要概念和实际应用。从基础的静态与非静态变量,到复杂的反射与动态实例化,理解这些概念有助于编写更高效、可维护的代码。持续学习和实践是掌握C#编程的关键,期待在未来的博客中与大家分享更多实用的技术知识。