一、复习委托事件
1、委托复习。
private delegate int MyDelegate(int a, int b); //1.定义委托类型
private static void Main(string[] args)
{
MyDelegate md = new MyDelegate(AddDelegate);//2.声明委托变量
int result = md(1, 2);//3.调用委托
Console.WriteLine(result);
Console.ReadKey();
}
private static int AddDelegate(int n1, int n2)//定义具体实现
{ return n1 + n2; }
问:上面1处定义委托类型时为什么不能用Static?
答:委托类型是一种定义方法签名的引用类型,用于引用方法或者匿名方法。委托类型本身
是静态的,并且在定义委托类型时不需要使用 static 关键字。委托类型是类级别的,而不
是实例级别的。
只有委托变量(即委托的实例)才能存储对具体实现方法的引用,它是可以用static
关键字。注意:静态委托变量只能调用静态方法,并且实例委托变量只能调用实例方法。
问:委托和事件有什么命名规则?
答:委托一般以“功能+Delegate”进行命名,例如MyDelegate,AddDelegate让人一眼知道使
用的是委托。
事件一般以On开始表示触发,或以Handler,Event为标志等等。例如,有一个名为
“ButtonClick” 的事件,可以选择将事件处理器命名为 “ButtonClickHandler”,或者将事
件触发方法命名为 “OnButtonClick”。
问:用lambda求List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6 };之和?
答: List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6 };
int sum = list.Sum(x => x);
上面是普通委托,下面扩展泛型委托
private delegate T MyDelegate<T>(T a, T b);
private static void Main(string[] args)
{
MyDelegate<double> md = new MyDelegate<double>(AddDelegate);
double result = md(3.2, 2.3);
Console.WriteLine(result);
Console.ReadKey();
}
private static double AddDelegate(double n1, double n2)
{ return n1 + n2; }
上面也可md=(x,y)=>x+y;无须定义类型,它会根据前面的double自动判断.
2、系统预定义委托
一般无须人为再定义委托,系统中有许多常用预定义的委托类型。
Action:用于引用无参数且无返回值的方法。Action、Action<T1, T2>
Func:用于引用具有返回值的方法。Func<TResult>、Func<T1, T2, TResult>
Predicate:用于引用返回布尔值的方法。Predicate<T>`
EventHandler:用于处理事件的委托类型,通常与事件一起使用EventHandler<TEventArgs>
Comparison:用于比较两个对象并确定它们的相对顺序的委托类型。Comparison<T>
Action<T>:用于引用具有一个参数且无返回值的方法。Action<T>
Converter:用于将一个类型的对象转换为另一个类型的对象的委托类型。
Converter<TInput, TOutput>
3、委托的好处:把方法作为参数。
可以是形参,也可以是返回类型。
private static void Main(string[] args)
{
M2((x, y) => (x + y).ToString());
M2((x, y) => (x * y).ToString());
Func<string, string, string> f = M3();//a
Console.WriteLine(f("好", "兄弟"));
Console.ReadKey();
}
private static void M2(Func<int, int, string> fn)
{ Console.WriteLine(fn(110, 120)); }
private static Func<string, string, string> M3()//方法作为返回类型
{ return (x, y) => x + y; }
注意a处写成M3将出错,为M3()
也可以作字段
private static void Main(string[] args)
{
MyClass mc = new MyClass();
mc.MsgDelegate = (x) => Console.WriteLine(x);
mc.MsgDelegate("类中作字段");
Console.ReadKey();
}
private class MyClass
{
public Action<string> MsgDelegate;
}
4、理解事件相关术语。发布,订阅,注册,触发等
事件模型通常是使用委托和事件关键字来实现的。发布者定义一个事件,监听者可以通
过订阅该事件来接收通知,并在事件被触发时执行相应的方法。
事件机制的基本思想是发布者和监听者之间的解耦,使得发布者不需要关心谁在监听事
件,监听者也不需要关心事件何时发布。监听者只需订阅感兴趣的事件,并在事件发生时执
行相应的逻辑。这种机制增加了代码的灵活性和可维护性,使得不同组件之间可以更松散地
耦合。
(1)事件(Event):事件是系统中发生的某种事情或状态的表示。它可以是用户交互、消息、
信号等。事件通常由发布者触发。
(2)发布者(Publisher):也称为事件源(Event Source),发布者是生成并触发事件的实
体。它负责将事件通知给订阅者。发布者可能是某个对象、组件、服务等。
(3)订阅者(Subscriber):订阅者是对特定事件感兴趣并注册了接收事件通知的实体。订阅
者可以是单个对象、组件、服务、事件监听器等。当事件发生时,订阅者的相关操
作将被执行。又称为监听者(Listener)或观察者(Observer)。
(4)注册(Registration):注册是将订阅者与事件关联起来的操作。通过注册,订阅者告诉
发布者它对某个事件感兴趣,并提供处理事件的逻辑。这样,在事件发生时,发布
者可以将事件通知给已注册的订阅者。
当事件发生时,发布者会将事件通知给已注册的订阅者。每个订阅者会根据其订阅的事
件类型,执行相应的处理逻辑。这些响应可以是更新UI界面、执行特定的功能、触发其他事
件、发送消息等等,具体取决于订阅者的实现。
例如,在一个简单的GUI应用程序中,当用户点击一个按钮时,按钮的“点击”事件将被
触发。应用程序中的不同模块可能会订阅这个按钮的点击事件。某个模块可能负责更新UI界
面,另一个模块可能执行某种业务逻辑,还可能有其他模块监听该事件并做出相应的响应。
例1:在发布者MyClass定义事件及触发事件,在Main中注册并用Lambda订阅,最后触发。
internal class Program
{
private static void Main(string[] args)
{
MyClass mc = new MyClass();
mc.msg += x => Console.WriteLine(x);//注册事件
mc.OnShowMsg("由方法传参到事件中");//触发
Console.ReadKey();
}
}
internal class MyClass//发布者
{
public event Action<string> msg;//事件
public void OnShowMsg(string s)//触发事件的方法,专门隔绝内部的事件触发
{
if (msg != null)
{
msg(s);//事件触发
}
}
}
例2:在发布者Counter定义countChange事件,在订阅者Subscriber构造中注册事件,并特
地用一个注销事件方法。事件触发时显示信息。
internal class Program
{
private static void Main(string[] args)
{
Counter counter = new Counter();
Subscriber subscriber = new Subscriber(counter);
counter.Count = 2; //计数发更变化2
counter.Count = 3; //计数发更变化3
subscriber.UnSubscriber();//注销后不再触发事件
counter.Count = 4;
Console.ReadKey();
}
}
internal class Counter//发布者
{
private int counter;
public event EventHandler countChange;//事件
public int Count
{
get { return counter; }
set
{
counter = value;
OnCountChanged();//触发事件的方法
}
}
public void OnCountChanged()//触发事件的方法
{//必须与事件触发隔开,保证安全性,不允许直接操作事件
if (countChange != null)
{
countChange(this, EventArgs.Empty);//事件触发
}
}
}
internal class Subscriber//订阅者
{
private Counter counter;
public Subscriber(Counter c)//构造
{
counter = c;
counter.countChange += ShowCounterMsg;//注册
}
public void UnSubscriber()
{
counter.countChange -= ShowCounterMsg;//注销
}
private void ShowCounterMsg(object sender, EventArgs e)//具体处理
{
Console.WriteLine($"计数发更变化{counter.Count}");
}
}
二、反射复习
1、反射也是一种机制。
事件是一种机制,用于实现对象之间解耦的机制。发布者<-->订阅者,让两者分离解耦.
反射是一种机制,允许程序在运行时,动态获取、检查、操作类型(类,结构体,接口等)
和成员(属性,方法,字段等)的信息。
通过反射,我们可以在运行时探查和分析代码,以及执行一些高级的动态操作。它常用于一
些需要在运行时根据不同条件加载和执行代码的场景,如插件系统、动态代码生成、
ORM(对象关系映射)工具等。
2、反射练习
(1)建立一个类库Driver,添加代码:
public class Driver
{
private string _name;
public int Age { get; set; }
public void ShowMessage(string s)
{
Console.WriteLine(s);
}
public string ShowMessage(string s1, string s2)
{
return s1 + s2;
}
}
public interface IGetable
{
void GetData();
}
(2)再建项目Test,添加代码:
private static void Main(string[] args)
{
//1.加载程序集
Assembly asm = Assembly.LoadFile(@"D:\OneDrive\CSharp\Test\Driver\bin\Debug\Driver.dll");
//2.取得类型
Type[] types = asm.GetTypes();
foreach (Type t in types) { Console.WriteLine(t.FullName); }
Type type = asm.GetType("Driver.Driver");
//3.取得方法
MethodInfo[] methods = type.GetMethods().Where(m => m.Name == "ShowMessage").ToArray();
for (int i = 0; i < methods.Length; i++)
{ Console.WriteLine(methods[i].ReturnParameter); }//判断有返回值的
ParameterInfo[] pis = methods[1].GetParameters();
foreach (ParameterInfo pi in pis) { Console.WriteLine(pi.ParameterType); }//查找返回类型
//运行方法
Object obj = Activator.CreateInstance(type);//a
object result = methods[1].Invoke(obj, new object[] { "大运会", "黑卡" });
Console.WriteLine(result);
Console.ReadKey();
}
基本上反射就是摸着石头过渡,你不知道里面有什么,但可以逐步列举,再进行具体确定调用。
问:上面a处的Object与object有什么区别?
答:没有区别。
实际上是完全相同的类型别名.无论你使用Object还是object,它们都表示.NET Framework
中基类类型。这两者之间在功能和使用上没有区别。
有意思的是,在C#中代码会推荐简写为object。因为它符合C#的命名约定和代码风格。
而在与其他面向对象编程语言进行兼容性或迁移代码的情况下,使用Object可能更合适。
3、Type类的使用
通过类获得Type:
Type t = typeof(Person)
通过对象获得类的Type:
Type t = p.GetType()
“c:\abc.dll”
Assembly asm=Assembly.LoadFile(“c:\abc.dll”);
调用Assembly的GetExportedTypes方法可以得到Assembly中定义的所有的public类型。
调用Assembly的GetTypes()方法可以得到Assembly中定义的所有的类型。
调用Assembly的GetType(name)方法可以得到Assembly中定义的全名为name的类型信息。
插件:扩展主程序功能的dll.
三、Type类的其它方法(反射继续)
1、动态创建对象
Activator.CreateInstance(Type t)会动态调用类的无参构造函数创建一个对象,返回值
就是创建的对象,如果类没有无参构造函数就会报错。
GetConstructor(参数列表);//这个是找到带参数的构造函数。
2、Type类的方法:在编写调用插件的程序时,需要做一系列验证。
bool IsAssignableFrom(Type c):(直译:是否可以从c赋值)判断当前的类型的变量是
不是可以接受c类型变量的赋值。
typeof(IPlugin).IsAssignableFrom(t)
bool IsInstanceOfType(object o):判断对象o是否是当前类的实例
(当前类可以是o的类、父类、接口)
bool IsSubclassOf(Type c):判断当前类是否是类c的子类。类的事,没有接口的事。
IsAbstract,判断是否为抽象的,含接口
(1)Type.IsAssignableFrom(Type c)方法可以帮助我们确定两个类型之间的继承关系,
判断一个类型是否是另一个类型的父类或子类,或者是否实现了某个接口。
internal class Program
{
private static void Main(string[] args)
{
Type typeInterface = typeof(IDrawable);
Type typeTeacher = typeof(Teather);
Type typeStudent = typeof(Student);
bool b1 = typeStudent.IsAssignableFrom(typeTeacher);//检查继承关系
bool b2 = typeTeacher.IsAssignableFrom(typeStudent);
bool b3 = typeStudent.IsAssignableFrom(typeInterface);
bool b4 = typeInterface.IsAssignableFrom(typeTeacher);
bool b5 = typeTeacher.IsAssignableFrom(typeInterface);
Console.WriteLine($"{b1},{b2},{b3},{b4},{b5}");
Console.ReadKey();
}
}
internal interface IDrawable
{ }
internal class Teather : IDrawable
{ }
internal class Student : Teather
{
public static int Age { get; set; }
public static void show()
{ Console.WriteLine("静态"); }
}
internal abstract class Myclass1
{
public abstract void MethodA();
public void MethodB()
{ }
}
internal static class Myclass2
{ public static string Name { get; set; } }
(2)Type.IsInstanceOfType(Object o)是Type类的一个方法,用于检查一个对象是否是
指定类型或其派生类型的实例。它返回一个布尔值,如果对象是给定类型的实例或其派
生类型的实例,则返回true,否则返回false。
//检查是否该类型的实例
object objInterface = Activator.CreateInstance(typeTeacher);//a
object objTeacher = Activator.CreateInstance(typeTeacher);
object objStudent = Activator.CreateInstance(typeStudent);
Console.WriteLine(typeInterface.IsInstanceOfType(objInterface));
Console.WriteLine(typeTeacher.IsInstanceOfType(objTeacher));
Console.WriteLine(typeStudent.IsInstanceOfType(objStudent));
Console.WriteLine(typeStudent.IsInstanceOfType(objTeacher));
Console.WriteLine(typeStudent.IsInstanceOfType(objStudent));//b
注意:a处不能直接接口实例化,因为接口是规则,应由该接口的类来实例化。
b处其派生的实例也是父类的实例。
(3)Type.IsSubclassOf(Type c)方法用于判断一个类型是否是另一个类型的子类,
它返回一个布尔值,表示是否是子类。
Console.WriteLine(typeTeacher.IsSubclassOf(typeInterface));//c
Console.WriteLine(typeStudent.IsSubclassOf(typeTeacher));
Console.WriteLine(typeTeacher.IsSubclassOf(typeStudent));
Console.WriteLine(typeTeacher.IsSubclassOf(typeTeacher));//d
注意:c处是false,虽然可把接口"当作"父类来理解,但它只是接口而非父类。
d处为false,自己不是自己的子类。
(4)Type.IsAbstract 属性用于判断特定类型是否为抽象的。当类型为抽象类、抽象接
口或抽象属性、方法的成员时,IsAbstract 返回 true,否则返回 false。
Type typeClass1 = typeof(Myclass1);
Type typeClass2 = typeof(Myclass2);
//object objmyClass1 = Activator.CreateInstance(typeClass1);//出错抽象不能实例化
Console.WriteLine("============");
Console.WriteLine(typeClass1.IsAbstract);//e true
Console.WriteLine(typeClass1.GetMethod("MethodA").IsAbstract);//f true
Console.WriteLine(typeClass1.GetMethod("MethodB").IsAbstract);//g false
Console.WriteLine(typeClass2.IsAbstract);//g true
Console.WriteLine(typeClass2.GetMethod("get_Name").IsAbstract);//i false
MethodInfo[] methods = typeStudent.GetMethods().Where(m => m.Name.Contains("Age")).ToArray();
foreach (MethodInfo m in methods)
{ Console.WriteLine(m.Name); }//查看一下属性这个方法是怎么命名的
Console.WriteLine(typeStudent.GetMethod("get_Age").IsAbstract);//j false
Console.WriteLine(typeStudent.GetMethod("show").IsAbstract);//k false
总结:1.不能实例化的"类"都是抽象的为true,如:抽象类,静态类,接口等
2.未具体实现的方法,即没有实现体{}的都是抽象的,否则为false。
问:抽象属性有什么用?
答:抽象属性是抽象类或接口中声明的属性,它们没有具体的实现,并要求派生类或实现
类提供具体的实现。
它的作用,实则就是抽象类实现的功能,如:强制继承类提供属性的具体实现,提供
统一的访问方式,封装实现细节,提供多态性等等
internal class Program
{
private static void Main(string[] args)
{
AbstractClass abstractObj = new DevicedClass();
abstractObj.MyAbstractProperty = 10;
Console.WriteLine(abstractObj.MyAbstractProperty);
Console.ReadKey();
}
}
internal abstract class AbstractClass
{
public abstract int MyAbstractProperty { get; set; }
}
internal class DevicedClass : AbstractClass
{
private int _privatevalue;
public override int MyAbstractProperty
{
get { return _privatevalue; }
set { _privatevalue = value; }
}
}
问:抽象事件可以在抽象类定义触发吗?
答:不能。
public abstract class Animal
{
public abstract event EventHandler<EventArgs> MoveEvent;
protected void RaiseMoveEvent(EventArgs e)
{
MoveEvent?.Invoke(this, e);//a 只能出现在+=或-=的左边
}
public abstract void Move();
}
public class Dog : Animal
{
public override event EventHandler<EventArgs> MoveEvent;
public override void Move()
{
Console.WriteLine("Dog is moving.");
RaiseMoveEvent(new EventArgs());
}
}
public class MainClass
{
public static void Main()
{
Dog myDog = new Dog();
myDog.MoveEvent += new EventHandler<EventArgs>(HandleMoveEvent);
myDog.Move();
}
private static void HandleMoveEvent(object sender, EventArgs e)
{
Console.WriteLine("Animal has moved.");
}
}
上面a处报错。改整个RaiseMoveEvent方法移到Dog类内部。程序正常。
3、EventInfo类介绍
EventInfo类是C#中用于反射事件的类型,它提供了访问和操作事件的属性和方法。
属性:
Name:获取事件的名称。
DeclaringType:获取声明该事件的类型。
EventHandlerType:获取事件处理程序的类型。
AddMethod:获取添加事件处理程序的方法。
RemoveMethod:获取移除事件处理程序的方法。
EventType:获取事件的类型。
方法:
AddEventHandler(Object target, Delegate handler):向特定对象的事件添加一
个事件处理程序。
GetAddMethod():返回表示添加事件处理程序的方法的MethodInfo对象。
GetRemoveMethod():返回表示移除事件处理程序的方法的MethodInfo对象。
GetRaiseMethod():返回表示引发事件的方法的MethodInfo对象。
GetCustomAttributes(bool inherit):返回应用于事件的自定义属性数组。
public class MyClass
{
public event EventHandler MyEvent;
public void RaiseEvent()
{
MyEvent?.Invoke(this, new EventArgs());//e
}
}
public class MainClass
{
public static void Main()
{
EventInfo ei = typeof(MyClass).GetEvent("MyEvent");
Console.WriteLine($"{ei.Name},{ei.DeclaringType},{ei.EventHandlerType}");
Console.WriteLine("=========");
MethodInfo addmethod = ei.AddMethod;
Console.WriteLine($"{addmethod.IsStatic},{addmethod}");
MyClass example = new MyClass();
addmethod.Invoke(example, new object[] { new EventHandler(MyEventHandler) });//a
example.RaiseEvent();//b
MethodInfo removemethod = ei.GetRemoveMethod();//c
Console.WriteLine($"{removemethod.IsPublic},{removemethod}");
removemethod.Invoke(example, new object[] { new EventHandler(MyEventHandler) });//d
Console.ReadKey();
}
private static void MyEventHandler(object sender, EventArgs e)
{
Console.WriteLine("Event handlered.");
}
}
上面a,d处不是执行事件,而执行add/remove方法,因为反编译委托后,有两个方法add
与remove。后面b处的raiseevent才是真正触发事件。
问:注释掉a处后,为什么b处仍然可以执行却不报错?
答:当注释a处后,没有添加方法到事件中去,因此MyEvent是空的。e处将不会执行,因此
也不会有什么提示。把e处改为:
if (MyEvent != null)
{
MyEvent.Invoke(this, new EventArgs());
}
else
{
Console.WriteLine("空事件!");
}
就可以在结果中看到“空事件”的提示了。
四、插件
1、问:可以随便给一程序写插件吗?
答:不能。
如果你想为某个程序编写插件,那么该程序必须提供一种机制来允许写入和加载插
件。对于C#语言来说,你可以采取以下几种方法来实现插件化的功能:
(1)插件接口和扩展点:程序中定义一个插件接口,定义插件必须实现的方法和属
性。程序通过动态加载插件,并根据插件接口调用插件提供的功能。这种方式需要程序
预留扩展点,即插件可以扩展的地方。
(2)DLL文件:将插件实现为独立的DLL文件,程序动态加载插件DLL,并使用插件提
供的功能。在C#中,可以使用反射机制动态加载和调用DLL。
(3)插件管理器:程序提供一个插件管理器,用于管理插件的加载、卸载和调用。
插件管理器可以提供插件注册、加载和通信的功能。
无论采用哪种方法,程序需要允许插件的加载和执行。这通常需要在程序的设计和
架构中预留相应的接口和扩展点,并提供必要的插件机制和API。
后面例子使用(2),会搜索当前程序目录下的addons文件夹里是否有dll文件,若有
则循环加载这些dll文件。这样实现了程序与插件的一体化。
插件编写通常使用插件框架来实现,插件框架提供了一种中间接口,用于连接原程
序和插件。这种中间接口定义了插件的规范和标准,以及插件可以调用的原程序接口。
在插件框架中,通常有一个接口,它定义了插件可以调用的原程序的方法。这个接
口应该在原程序中被实现,以便插件可以使用它。同时,这个接口也应该在插件中被实
现,以便原程序可以调用插件中的方法。
为了实现这种关系,可以使用C#中的接口和抽象类。在原程序中,可以定义一个接
口,该接口定义了插件可以调用的方法。然后,可以在插件中创建一个类,该类实现这
个接口,以便插件可以使用它。
在原程序中,需要加载插件,并将其与接口关联起来。这可以通过反射来实现,使
用反射来获取已加载的插件中的实现类,并将其与接口关联起来。这样,原程序就可以
使用插件中的方法了。
为了确保插件的安全性,需要对插件进行验证和签名。这样,只有经过验证和签名
的插件才能被加载到原程序中。
总结:在插件编写中,需要定义一个中间接口,它既连接原程序,又规定了插件的
方式。这个接口应该在原程序中被实现,并在插件中被实现。为了实现这种关系,可以
使用C#中的接口和抽象类,并使用反射来加载插件。同时,需要对插件进行验证和签名,
以确保其安全性。
2、实例:给格式菜单添加一“转换大小”菜单,点击并实现功能。
分三部分:原程序<-->接口<--->插件
接口是媒婆,连接原程序与插件。程序按接口进行调用,插件按接口标准进行编写。
(1)原程序Form,菜单与textbox
加载时就搜索当前目录addons下是否有dll插件,有就加载调用。
private void Form1_Load(object sender, EventArgs e)
{
string Apath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);//程序目录
string dpath = Path.Combine(Apath, "addons");//程序目录下面addons的目录
string[] dlls = Directory.GetFiles(dpath, "*.dll");//查找全部dll文件
foreach (string dll in dlls)
{
Assembly asm = Assembly.LoadFile(dll);//逐个加载程序集
Type[] types = asm.GetTypes();//获取全部类型
Type typeEditor = typeof(IEditor);//接口
foreach (Type t in types)
{
if (typeEditor.IsAssignableFrom(t) && !t.IsAbstract)//若为接口类型
{
IEditor objEditor = (IEditor)Activator.CreateInstance(t);//创建接口对象
ToolStripItem tsi = tsmiFormat.DropDownItems.Add(objEditor.Name);//添加菜单项
tsi.Tag = objEditor;//储存接口对象,以便传递到事件处理中
tsi.Click += Tsi_Click;//注册事件
}
}
}
}
private void Tsi_Click(object sender, EventArgs e)//触发事件
{
ToolStripItem tsi = (ToolStripItem)sender;//发送者转为正常对象
IEditor objEdito = (IEditor)(tsi.Tag);//提取发送者时的tag,即真正的接口
objEdito.Run(this.textBox1);//事件触发
}
(2)接口MyNotepadInterface.一个类库dll,最后需要引用到程序与插件中。
规范程序与插件,让大家明明白白"消费",彼此知道怎么通信调用。
namespace MyNotepadInterface
{
public interface IEditor//接口,连接程序与插件dll
{
string Name { get; }//需要一个名字
void Run(TextBox txtBox);//需要一个带参Run方法
}
}
(3)插件dll.具体写明做什么,方便程序来调用。也是一个类库dll.
namespace MyDll.ToUpper
{
public class ToUpper : IEditor//插件 根据接口制作,以便对接程序
{
public string Name
{
get { return "转换大写"; }//具体名字
}
public void Run(TextBox txtBox)
{
txtBox.Text = txtBox.Text.ToUpper();//具体操作
}
}
}
问:ToolStripItem和ToolStripMenuItem有什么区别?
答:两者都是C#中用于创建菜单和工具栏的类,区别如下:
(1)继承关系:
ToolStripItem是所有菜单项和工具栏项的基类,它还包括ToolStripButton、
ToolStripLabel等等。
ToolStripMenuItem是ToolStripItem的子类,它是专门用于创建菜单项的类。
(2)UI外观:
ToolStripItem的外观比较通用,可以用于创建菜单项、工具栏按钮、标签等。它
通常是一个单独的元素,可以包含文本、图标和其他可定制的属性。
ToolStripMenuItem是ToolStripItem的扩展,专门用于创建菜单项。它在外观上更
接近传统的下拉菜单样式,可以包含子菜单项和分隔符。
(3)层级结构:
ToolStripItem是一个相对独立的菜单项,没有内置的支持来创建层级结构的菜单。
ToolStripMenuItem具有内置的支持来创建层级结构的菜单,可以添加子菜单项和
分隔符,以创建更复杂的菜单布局。
如果你只需要简单的菜单项或工具栏项,可以使用ToolStripItem类。如果你需要
创建下拉菜单,并支持层级结构、子菜单、分隔符等功能,则用ToolStripMenuItem类。
问:AppDomain是什么?
答:应用程序域。作了解即可,也许一辈子都不会用到。
.Net框架系统加载后,就会有系统域,同时根据动态会创建共享域与默认域,当程
A运行时,就会在默认域去登记入口地址等,相当于报备:A运行了。
A程序启动时,会先创建一个主进程,主进程会创建一个主应用程序域。程序A可能
有其它进程。一个应用程序域只能有一个exe,但可以加载多个dll。程序A可能有多个
应用程序域,彼此间一般无法通信。每个应用程序域内有数据,代码,包括线程。在应
用程序域中可以有多个线程,同一时间一个线程只能属于一个应用程序域,不同时刻线
程可以在另一个应用程序集中运行,即线程可以穿透应用程序域。
获得当前应用程序运行根目录
string path = AppDomain.CurrentDomain.BaseDirectory;
上面很枯燥,无所谓,了解一下即可,形象比喻:
假设我们要组织一个大型音乐会,可以使用以下形象的例子来说明程序、进程、
AppDomain和线程的关系:
(1)程序:音乐会的计划书
程序就像是音乐会的计划书,它包含了所有的细节和指令,如演出曲目、演出时间、
舞台布置等。计划书是为了让音乐会能够顺利进行,而程序是为了让计算机能够执
行任务。
(2)进程:音乐会的组织团队
进程就像是音乐会的组织团队,他们负责协调和管理整个音乐会的筹备工作。组织
团队可以有多个成员,每个成员负责不同的任务,如舞台搭建、音响调试等。进程
是为了让程序能够在计算机上运行。
(3)AppDomain:音乐会的不同场地
AppDomain就像是音乐会的不同场地,每个场地都有自己的舞台和观众席。不同的
场地可以同时举行不同的音乐会,观众互不干扰。同样,不同的AppDomain可以同
时加载和执行不同的程序集,实现功能的扩展和隔离。
(4)线程:音乐会的演奏者
线程就像是音乐会的演奏者,他们负责演奏乐曲,给观众带来美妙的音乐。演奏者
可以同时演奏不同的乐器,实现和声和多声部的效果。同样,线程可以同时执行不
同的代码路径,实现并行执行和任务分配。
程序就是音乐会的计划书,进程是音乐会的组织团队,AppDomain是音乐会的不同
场地,线程是音乐会的演奏者。程序提供了指导和指令,进程负责协调和管理,
AppDomain提供隔离和资源管理,线程执行具体的任务。它们之间相互配合,共同完成
音乐会的筹备和演出。当音乐会结束时,组织团队解散,场地关闭,演奏者离开舞台。
同样,当程序退出时,进程、AppDomain和线程都会被销毁,释放占用的资源。
3、问:插件必须是一个程序集吗?
答:不一定。
插件并不一定必须是一个程序集。插件可以是任何可执行代码或动态链接库(DLL),
包括类库、函数库、框架等。程序集是一种将多个类型和资源打包到一个文件中以便重
用的机制,它可以是动态链接库或静态链接库。对于插件来说,它可以是任何可执行代
码或动态链接库,可以是一个程序集或其他类型的的数据结构。插件系统可以加载和调
用这些可执行代码或动态链接库,以便插件可以实现自定义和扩展应用程序的功能。
internal interface IPlugin//a 插件接口
{
void Dosomething();//规定插件标准,有无参无返回值的方法
}
internal class PluginManager//b 插件处理
{
private static IPlugin instance;
public static void Setplugin(IPlugin plugin)
{
instance = plugin;//前面准备工作
}
public static void Dosomething()//c
{
instance?.Dosomething();//调用插件
}
}
internal class MyPlugin : IPlugin //插件,具体实现
{
public void Dosomething()
{
Console.WriteLine("这里是具体插件的实现"); ;
}
}
internal class Program
{
private static void Main(string[] args)
{
MyPlugin plugin = new MyPlugin();
PluginManager.Setplugin(plugin);
PluginManager.Dosomething();
Console.ReadKey();
}
}
上面b处添加接口IPlugin将出错。接口添加后,因为静态成员不能被继承或实现。接口
定义的方法必须是实例方法,而不是静态方法。因此,尝试在实现接口时将方法声明为
静态会导致编译错误。
下面例子,把程序与接口放在一起,另外单独做一个插件dll
(1)程序与接口
public interface IPlugin
{
void Dosomething();
}
public class PluginManager//b 插件处理
{
private static IPlugin instance;
public static void Setplugin(IPlugin plugin)
{
instance = plugin;
}
public static void Dosomething()
{
instance?.Dosomething();
}
}
internal class PluginLoader
{
public static void Load()
{
string p = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string dllp = Path.Combine(p, "addons");
string[] dlls = Directory.GetFiles(dllp, "*.dll");
foreach (string dll in dlls)
{
Assembly asm = Assembly.LoadFrom(dll);
Type[] types = asm.GetTypes();
Type typePlugin = typeof(IPlugin);
foreach (Type type in types)
{
if (typePlugin.IsAssignableFrom(type) && !type.IsAbstract)
{
IPlugin objPlugin = (IPlugin)Activator.CreateInstance(type);
PluginManager.Setplugin(objPlugin);
}
}
}
}
}
internal class Program
{
private static void Main(string[] args)
{
PluginLoader.Load();
PluginManager.Dosomething();
Console.ReadKey();
}
}
提示,因为接口要跨程序集,因此应为public,否则插件中无法引用接口.
(2)单独做一个插件.生成后dll放在上面程序当前目录addons中.
namespace Plugin.Do
{
public class Do : IPlugin
{
public void Dosomething()
{
Console.WriteLine("插件里做一点事。。。。");
}
}
}
4、问:插件的思想与委托又有什么区别与联系呢?
答:插件思想和委托都是一种将程序的可扩展性和灵活性引入到应用程序中的技术。
插件思想是一种将应用程序的功能模块化的设计方式,它将应用程序的功能拆分成
多个独立的插件,每个插件可以实现不同的功能,并且可以动态地加载和卸载。这种设
计方式使得应用程序的功能可以通过插件进行扩展和修改,而不需要修改应用程序的源
代码。
委托是一种将方法作为参数传递的技术,它可以将多个方法封装到一个委托对象中,
并且可以像调用单个对象一样调用委托对象中的方法。这种技术可以用于实现事件处理、
异步调用、回调函数等功能,并且可以方便地进行代码重用和跨平台操作。
插件思想和委托都是为了实现程序的可扩展性和灵活性,但它们实现的方式不同。
插件思想是通过将程序的功能模块化、动态加载和卸载插件来实现的,而委托是通过将
方法作为参数传递、封装多个方法来实现的。这两种技术可以结合使用,例如可以使用
委托来实现插件之间的通信,或者将委托作为插件的接口来实现插件之间的交互。
5、问:[Flages]有什么作用?
答:[Flages]仅用于枚举类型的一个C#特性。用于将枚举类型标记为可组合的。这意味
着可以将多个枚举成员组合成一个整数值,并且在比较或存储时可以像使用单个枚举成
员一样使用它们。
[Flags] 属性对于枚举类型有两个主要用处:
(1)可组合性:
使用 [Flags] 属性可以将枚举成员标记为可组合的,这意味着可以将多个成员组合
成一个整数值。这样可以在需要同时使用多个成员的情况下,将它们组合在一起,而不
是分别使用每个成员。这种可组合性使得枚举类型更加灵活和有用。
(2)位运算符:
与可组合性相关的是,使用 [Flags] 属性还可以使用位运算符来操作枚举类型。
位运算符允许对枚举成员进行位级别的操作,例如按位与、按位或、按位异或等。这些
操作可以用于将多个成员组合成一个整数值,或者对整数值进行拆分和检查特定的位。
internal class Program
{
private static void Main(string[] args)
{
Weekdays1 w1 = Weekdays1.Monday;
int n = 3;
Console.WriteLine($"{w1},{(int)w1},{(Weekdays1)n}");//Monday,1,Tuesday
w1 = Weekdays1.Monday | Weekdays1.Tuesday;
Console.WriteLine($"{w1},{(int)w1}");//Monday, Tuesday,3
Weekdays2 w2 = Weekdays2.Monday | Weekdays2.Tuesday;
Console.WriteLine($"{w2},{(int)w2}");//3,3 区别不会罗列各成员
if ((w1 & Weekdays1.Monday) != 0) Console.WriteLine(Weekdays1.Monday);
if ((w1 & Weekdays1.Tuesday) != 0) Console.WriteLine(Weekdays1.Tuesday);
if ((w2 & Weekdays2.Monday) != 0) Console.WriteLine(Weekdays2.Monday);
if ((w2 & Weekdays2.Tuesday) != 0) Console.WriteLine(Weekdays2.Tuesday);
Console.ReadKey();
}
}
[Flags]
internal enum Weekdays1
{
Monday = 1,
Tuesday = 2,
Wednesday = 4,
Thursday = 8,
Friday = 16,
Saturday = 32,
Sunday = 64
}
internal enum Weekdays2
{
Monday = 1,
Tuesday = 2,
Wednesday = 4,
Thursday = 8,
Friday = 16,
Saturday = 32,
Sunday = 64
}
当将一个数值转为枚举时,枚举值中小于该数值的最大成员将被返回。以本题为例,3转
枚举时,最靠近的小于3的枚举成员是第二个成员,其值为2。
上面按位与的前提是:1成员互斥,2多成员之和也互斥。这样只要按位与相同才不为0,不
同时肯定就是0,因此只要不为0,就是有该成员。
多成员之和也互斥,一般都设置为2的幂。
复习枚举:
枚举成员从 0 开始,以 1 为增量直到最后一成员。各成员值互斥不同是相同值。可
人为改变其中各值,未设置的值将按1增量向下。
enum weekdays
{
Monday,
Tuesday = 5,
Wednesday,
Thursday = 10,
Friday,
}
Monday的值为0,Tuesday的值为5,Wednesday的值为6,Thursday的值为10,Friday的值为11。
人为不必增量,可以降序赋值,甚至乱序赋值,只要互斥即可:
internal enum Weekdays2
{
Monday = 1,
Tuesday = 2,
Wednesday = 4,
Thursday = 3,
Friday = 9,
Saturday = 8,
Sunday = 7
}
枚举类型的底层类型可以是任何基本数据类型,包括 byte, sbyte, short, ushort, int,
uint, long, or ulong等。
internal enum TemperatureUnits : long
{
Celsius,
Fahrenheit,
Kelvin
}
如果不指明枚举类型的底层类型,则默认使用int作为底层类型。
问:为什么不用double作为底层类型?
答:由于双精度浮点数的精度可能会受到计算机内部表示和计算误差的影响,因此每个枚举
成员的默认值可能会有微小的变化,容易造成误差。
由于枚举类型实际上是整数类型的结构体,因此它们的底层类型必须是整数类型。
应用:下面abcd处输出的结果是多少?
private static void Main(string[] args)
{
Friends g = Friends.男 | Friends.女;
Console.WriteLine($"{g},{(int)g}"); //a
g = Friends.女 | Friends.男;
Console.WriteLine($"{g},{(int)g}");//b
//
Sharps s = Sharps.小 | Sharps.大;
Console.WriteLine($"{s},{(int)s}"); //c
s = Sharps.大 | Sharps.小;
Console.WriteLine($"{s},{(int)s}");//d
Console.ReadKey();
}
[Flags]
internal enum Friends
{
男,
女
}
internal enum Sharps
{
大,
小
}
结果:a和b:女,1
c和d:小,1
提示:用|表示两值相加。男为0,女为1,两者和为1,恰好为女的值,故显示为女。
若改下面:
internal enum Sharps
{
大=1,
小=2
}
则c和d结果为:3,3。因为大为1,小为2,两者和为3,无对应枚举直接显示数字。
6、BindingFlags枚举类型
BindingFlags 是 C# 中一个用于指定绑定标志的枚举类型。它用于在反射操作中控制绑定
行为,并可以指定以下标志:
None:没有绑定标志。
FlattenHierarchy:将祖先类型(基类)的成员也包括在绑定结果中。
DeclaredOnly:只包括当前声明的成员,不包括继承的成员。
Instance:只包括实例成员,不包括静态成员。
Static:只包括静态成员,不包括实例成员。
Public:只包括公共成员,不包括受保护成员或私有成员。
NonPublic:包括受保护成员、私有成员和其他所有非公共成员。
Override:在基类和接口中查找重写方法。
IgnoreCase:在名称比较时忽略大小写。
通过使用 BindingFlags,您可以在反射操作中更精确地控制要绑定的到的成员,从而更好
地控制代码的动态行为。
internal class Program
{
private static void Main(string[] args)
{
Type type = typeof(Person);
MethodInfo m1 = type.GetMethod("SayHi");
m1.Invoke((Person)Activator.CreateInstance(type), null);
//MethodInfo m2 = type.GetMethod("Show");//此句为null,故下面方法报错
//m2.Invoke((Person)Activator.CreateInstance(type), null);
MethodInfo m3 = type.GetMethod("Show", BindingFlags.NonPublic | BindingFlags.Instance);//此句为null,故下面方法报错
m3.Invoke((Person)Activator.CreateInstance(type), null);
Console.ReadKey();
}
}
public class Person
{
private void Show()
{ Console.WriteLine("私有方法"); }
public void SayHi()
{ Console.WriteLine("公有方法"); }
}
7、自动属性
自动属性(Auto-implemented Properties)是一种简化属性定义的语法。它允许你在
类中定义属性,而无需显式地编写相应的私有字段。
(1)语法格式:
public <type> <PropertyName> { get; set; }
可以用public、protected、internal或private关键字指定访问修饰符。
(2)简化属性的定义:
通过使用自动属性,你无需手动编写字段和属性的Getter和Setter方法的代码。
C#编译器会自动为你生成。
(3)默认生成的私有字段:
C#编译器会为自动属性生成一个私有字段,自动属性的字段名称由编译器自动生
成,通常为"属性名_属性类型"。自动属性的字段名称是编译器内部的实现细节,
通常不需要直接使用。在代码中使用属性名称来访问自动属性的值,而不需要显
式引用对应的字段名称。
(4)只读和只写属性:
如果想要只读属性,可省略set访问器。如果想要只写属性,可省略get访问器。
public string Name { get; } // 只读属性
public int Age { set; } // 只写属性
只读属性的值可以在构造函数内初始化,之后不可更改。
(5)初始化器:
C# 6.0及更高版本支持自动属性的初始化器。可以在声明时给属性提供默认值。
public string Name { get; set; } = "John";
public int Age { get; set; } = 18;
(6)注意:
自动属性本质上是将字段和属性的定义合并在一起,因此它们本质上是公共的,
可以在类的外部访问。
自动属性的Getter和Setter方法没有显式定义,因此你无法在其内部执行额外的
逻辑。
自动属性适用于那些不需要进一步扩展的简单属性。如果你需要在Getter或
Setter方法中添加额外的逻辑,你需要使用完整的属性定义。
自动属性不适用于需要在Getter和Setter方法中有不同的访问修饰符的场景。
此时,你需要使用完整的属性定义。
8、隐式类型(复习)
隐式类型(Implicitly Typed)是一种类型推断机制,它允许你在编译时根据变量的
赋值自动推断变量的类型。
注意:这是有根据的推断,因此类型是正确的。在编译时已经能确定变量的类型,
并不是弱类型,而是强类型。只是方便程序员使用而已,不能作为类成员的类型、不能
用作方法的参数,返回值。(只能用作局部变量的类型推断)
(1)语法格式:
使用var关键字来声明隐式类型的变量,例如:
var variableName = initialValue;
(2)类型推断:
C#编译器会根据变量的初始化表达式自动推断变量的类型。这意味着你无需显式
指定变量的类型,编译器会自动根据初始化表达式决定变量的类型。
(3)注意:
隐式类型只能在声明变量时使用。一旦变量被赋予一个值,它的类型就会被固定,
不能再更改为其他类型。
注意,虽然变量的类型是隐式的,但它仍然是静态类型,编译器在编译时会对类
型进行检查。
(4)应用场景:
当变量的类型很长或复杂时,使用隐式类型可以减少代码的重复和冗长。
当变量的类型是明显的,并且初始化表达式中包含了足够的信息来确定类型时,
使用隐式类型可以简化代码并提高可读性。
(5)技巧:
使用隐式类型时,注重代码的可读性和可维护性。尽量选择有意义的变量名,以
使代码更易于理解。
不要滥用隐式类型,尤其是在代码复杂或需要与其他开发者共享代码时。确保代
码的类型推断仍然容易理解和维护。
var name = "John"; // 推断为字符串类型
var age = 30; // 推断为整数类型
var student = new Student("Alice", 20); // 推断为Student类型
var x;//错误
var y = null;//错误
var z = { 1, 2, 3 };//错误
var z=new[] {1,2,3};//正确。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (var number in numbers)
{
Console.WriteLine(number);
}
var students = GetStudents();
var teenagers = from student in students
where student.Age >= 13 && student.Age <= 19
select student;
var person = new { Name = "Tom", Age = 25 };
var result = GetResult();
// 不推荐以下使用方式,因为代码可读性较差
var dictionary = new Dictionary<string, List<int>>();
var query = from item in list
select new { Name = item.Name, Age = item.Age };
9、匿名类型(复习)
var p = new { name = "Tom", Age = 23 };//创建了一个匿名类型对象。
匿名类型是一种临时创建的只读对象,它具有一组只读的自动属性,这些属性的名
称和类型是根据初始化表达式自动推断的。
匿名类型对象是只读的,即无法在其创建后修改属性的值。它只能通过其属性进行
访问,但不能进行修改。
匿名类型对象通常用于临时存储一组相关的属性值,并且在需要强类型的对象的场
景通常用作临时数据传递。
匿名类型对象不可以直接定义方法,因为它们是只读的,不能在创建后添加新的成
员。如果需要在对象上定义方法,你可以考虑使用具名类型(Named Type)来定义一个
具名的类或结构体,然后在对象上定义方法。
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void SayHello()
{
Console.WriteLine($"Hello, my name is {Name} and I am {Age} years old.");
}
}
// 创建具名类型对象并调用方法
var person = new Person { Name = "Tom", Age = 23 };
person.SayHello();
注意,并非所有的场景都适合使用匿名类型或具名类型。具名类型通常用于定义具有
特定行为和状态的类型,而匿名类型则用于一些临时或简单的数据传递。
var p1 = new { Id = 1, Name = "YJingLee", Age = 22 };//属性也不需要申明
var p2 = new { Id = 2, Name = "XieQing", Age = 25 };
var intArray = new[] { 2, 3, 5, 6 };
var strArray = new[] { "Hello", "World" };
var objArray = new[] { new { Name = "YJingLee", Age = 22 }, new { Name = "XieQing", Age = 25 } };
var a = intArray[0];
var b = strArray[0];
var c = objArray[1].Name;
10、对象、集合初始化器(复习)
对象和集合初始化器是一种简化对象和集合创建过程的语法,它允许你在创建对象或集
合的同时初始化它们的属性或元素值。
(1)对象初始化器:
对象初始化器允许在创建对象时为其属性赋值。它的语法格式为
new ClassName { Property1 = value1, Property2 = value2, ... }
var person = new Person { Name = "Tom", Age = 25 };
Student stu= new Student(){ Id = 1, Name = "YJingLee", Age = 22 };
(2)集合初始化器:
集合初始化器用于在创建集合时添加元素。它的语法格式为
new CollectionType { element1, element2, ... }
var numbers = new List<int> { 1, 2, 3, 4, 5 };
List<int> num = new List<int> (){ 0, 1, 2, 6, 7, 8, 9 };
List< Student > stus= new List< Student >
{
new Student{Id=1,Name="YJingLee",Age=22},
new Student{Id=2,Name="XieQing",Age=25},
};
A a = {1, 2, 3}; // 错误,不能使用A的初始化器来初始化A的对象
A a = new A({1, 2, 3}); // 正确,
使用new表达式来初始化A的对象,但是可以使用一个包含A对象或者初始化器的new表
达式来进行初始化。
(3)易错处:
当属性或元素不存在时,会导致编译错误。
在使用集合初始化器时,需要确保提供的元素类型与集合类型相匹配,否则会导致
编译错误。
(4)场景:
对象初始化器可以用于创建和初始化对象的属性,使代码更简洁。
集合初始化器可以用于在创建集合时添加初始元素,提高代码可读性。
初始化器特别适用于在声明和初始化对象或集合时一起使用,减少代码的行数。
(5)技巧:
嵌套初始化器:对象或集合的属性也可以是对象或集合,可以通过嵌套初始化器为
它们的属性赋值。
可选属性赋值:可以通过使用`?`来设置属性的可选性,如果属性为可选,可以将
其赋值为`null`。
// 对象初始化器
var person = new Person { Name = "Tom", Age = 25 };
var order = new Order// 嵌套初始化器
{
OrderId = 1,
Customer = new Customer { Name = "John", Address = "123 Main St" },
Items = new List<Item>
{
new Item { Name = "Item 1", Price = 10 },
new Item { Name = "Item 2", Price = 20 },
new Item { Name = "Item 3", Price = 30 }
}
};
var numbers = new List<int> { 1, 2, 3, 4, 5 };// 集合初始化器
var person = new Person { Name = "Tom", Age = null };// 可选属性赋值
五、扩展方法
1、什么是扩展方法?
扩展方法是一种特殊的静态方法,可以为已存在的类添加新的方法,而无需修改原始类
的定义。扩展方法通过使用特殊的语法来实现,使得我们可以像调用实例方法一样调用
这些扩展方法。
扩展方法的目的是为了在不修改原始类的情况下,为其添加新的行为或功能。它通
常用于以下场景:
a.为第三方库或系统类添加新的方法,以便更方便地使用这些类。
b.在不可修改的类上添加新的方法,以提供更直观、更易用的接口。
c.在自己的类上添加新的方法,以提供更灵活的功能。
扩展方法的语法格式如下:
public static <返回类型> <扩展方法名>(this <扩展的类型> <扩展的对象>, <参数列表>)
{
// 方法实现
}
在扩展方法中,this关键字表示当前方法所扩展的类型。它用于指定要扩展的类或
结构的实例。
注意:
扩展方法必须定义在静态类中。
扩展方法必须是静态的。
扩展方法的第一个参数必须使用 this 关键字来修饰,指定扩展的类型。
扩展方法的第一个参数的类型就是扩展的类型。
扩展方法可以有任意多个参数。
扩展方法的重载是允许的,但是在使用时需要注意避免出现歧义的情况。如果存在多个
同名的扩展方法,编译器会选择最匹配的扩展方
静态类可以有静态方法,但静态类本身不能有扩展方法。只有非静态类才能定义扩展
方法。
2、什么情况下推荐使用扩展方法?
(1)对于已有的第三方或系统定义类,可以使用扩展方法为其添加新的功能,而无需继
承或修改原有的类。
(2)当某个功能经常被使用,但是原有类中没有相应的方法时,可以考虑使用扩展方法
来实现该功能。
(3)当需要为一个类添加的方法与该类的核心功能紧密相关时,可以使用扩展方法来增
强该类的功能。
简言之,不必为今后新版本可能与该方法同名,就不使用扩展方法。
注意两点:
(1)扩展方法,必须在顶级静态类中定义。不能在嵌套类中定义(类中有类)
(2)扩展方法必须在同一命名空间中定义,这是为了能够正确地将扩展方法与对应的类关
联起来。如果扩展方法定义在不同的命名空间中,需要使用using关键字将其引入。
即:必须保证扩展方法类已经在当前代码中using引入。
(3)扩展方法可以有不同的访问修饰符,包括public、private、internal等。然而,为
了能够在其他地方正确地调用扩展方法,通常建议将扩展方法定义为public或者
internal。
使用扩展方法时,我们可以像使用原始类的实例方法一样使用它们,但实际上它们只是
静态方法,并不会对原始类进行任何修改,扩展方法内部不能调用被扩展类的私有、
protected的东西。
3、扩展重载举例
namespace Test
{
internal class Program
{
private static void Main(string[] args)
{
string s = "Hello world! We-are coming!";
Console.WriteLine(s.WordCount());//4
Console.WriteLine(s.WordCount('-'));//2
Console.ReadKey();
}
}
internal static class StringExt//必须与指定类型在同一命名空间,当前Test
{
public static int WordCount(this string s)//只能位于静态类顶级
{
return s.Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries).Count();
}
public static int WordCount(this string s, char c)
{
return s.Split(new char[] { c }, StringSplitOptions.RemoveEmptyEntries).Count();
}
}
}
4、练习:自己编写一个Person类,该类中本身没有SayHello方法,通过扩展方法,为该类
增加一个SayHello方法
internal class Program
{
private static void Main(string[] args)
{
Person p = new Person { Name = "刀郎" };
Console.WriteLine(p.Say("聊斋"));
Console.WriteLine(p.SayHi());
Console.WriteLine(p.SayHi("新疆"));
Console.ReadKey();
}
}
internal class Person
{
public string Name { get; set; } = "Tom";
public string Say(string s)
{
return s;
}
}
internal static class PersonExt
{
public static string SayHi(this Person p)
{
return p.Name;
}
public static string SayHi(this Person p, string s)
{
return p.Name + s;
}
}
六、XML基础
1、XML(eXtensible Markup Language)可扩展标记语言,它长成什么样子?
<?xml version="1.0" encoding="UTF-8"?>
<students>
<stu>
<name>许万里</name>
<age>18</age>
<gender>男</gender>
</stu>
<stu>
<name>石国庆</name>
<age>19</age>
<gender>男</gender>
<address>北京房山</address>
</stu>
<stu>
<name>刘万仁</name>
<age>10</age>
</stu>
<stu>
<name>熊薇</name>
<age>11</age>
<gender>女</gender>
<emai1>xw@yahoo .com</emai1>
<status>单身</status>
<address>北京海淀</address>
</stu>
<stu>
<name>郎青祥</name>
</stu>
</students>
什么是标记语言?什么是标记?
标记 (markup):
文档中任何不想被打印输出的部分 (不是真正的文档的内容,联想读书时做的“读书笔
记”,在旁边写的注解等。)注解是注解,实际内容是实际内容。
标记的作用:
传递了关于文档本身以外的额外信息。比如:标记文档的某部分该如何显示,某部分是
什么意思等。重在数据,标记只是为了说明数据的含义而已。
常见的标记语言: SGML、HTML、XMLHTML与XML的区别与联系?
Xml作用与应用场合:
xml数据存储,html数据展示语法、是否有预定义标签、各自的作用与意义
相关术语:
标签、节点、根节点、元素、子元素、后代元素、属性嵌套、命名空间、字符数据 (CDATA)
2、什么是XML?
XML(Extensible Markup Language)是一种用于表示结构化数据的标记语言。它被设
计用于存储和传输数据,而不仅仅是用于显示数据。XML的优点包括其平台无关性、语言无
关性、可扩展性、自描述性以及灵活性。这些特性使得XML成为一种在各种不同系统之间进
行数据交换的理想格式。
XML的主要特点包括:
可扩展性:
XML允许用户定义自己的标记,因此它可以用于表示各种不同类型的数据。
平台无关性:
XML数据可以在不同的操作系统、不同的硬件平台上使用。
语言无关性:
XML数据可以使用各种不同的语言进行编写和阅读。
自描述性:
XML文档包含了数据本身的结构信息,因此它不需要外部的描述。
灵活性:
XML允许嵌套标记,使用属性,以及使用注释,因此它可以用于表示各种复杂的数据
结构。
在C#中,您可以使用System.Xml命名空间来处理XML数据。这个命名空间提供了一组类,
用于读取、写入、验证和操作XML文档。使用C#处理XML的一个主要优点是,您可以使用.NET
框架提供的的功能强大的类库来处理XML数据,而不需要手动解析XML文档。
XML的语法介绍:
XML语法包含以下元素:
标签:
XML标签由尖括号包围的标记组成,用于定义XML元素。例如,<name>是一个XML元素,
它由尖括号包围的“name”标记组成。
属性:
XML属性是定义XML元素属性的名称和值。属性在XML元素标签内部定义,并使用名称
和值之间的等号进行分隔。例如,<person name="John" age="30">中定义了两个属
性:name和age。
元素嵌套:
XML元素可以嵌套在其他元素内部,这允许定义复杂的数据结构。例如,<person>元
素可以包含<name>元素和<age>元素。
文本内容:
XML元素可以包含文本内容。例如,<name>John</name>`中的“John”是文本内容。
空白字符:
XML允许使用空格、换行符和制表符来分隔标签和属性。
XML语法规范:标签(Tag)、嵌套(Nest)、属性。标签要闭合,属性值要用""包围,
标签可以互相嵌套
XML树,父节点、子节点、兄弟节点(siblings)
XML应用场景主要有哪些?
XML的应用场景非常广泛,可以用于数据交换、数据存储、Web服务、数据库、配置文件
以及自定义标记等。
数据交换:
XML可以用于在不同的系统之间进行数据交换,因为它是一种平台无关、语言无关的数
据格式。
数据存储:
XML可以用于将数据存储在XML文件中,以便以后读取和操作。
Web服务:
XML可以用于构建和发送Web服务请求,以及解析和验证Web服务响应。
数据库:
一些数据库管理系统支持XML数据类型,允许存储和查询XML数据。
配置文件:
XML可以用于配置应用程序或系统,因为它具有结构清晰、易于阅读和编辑的特点。
.net程序中的一些配置文件app.config、web.config文件都是xml文件。
自定义标记:
XML允许用户定义自己的标记,因此可以用于表示各种不同类型的数据。
XML与HTML的区别是什么?
XML和HTML有着不同的目的和用法,它们并不是可以互相替代的。
目的:
XML是一种独立于软件和硬件的工具,用于传输和存储数据,关注数据的内容;HTML用
于显示数据并关注数据的外观。
标记定义:
XML提供了一个定义标记语言的框架,而HTML本身就是一种标记语言。
根元素:
两者有且只能有一个根元素。HTML的根元素是<html>,它包含了整个HTML文档的结构
和内容。在XML中,根元素也是必需的,它的名称可以由用户自行定义,根元素不能包
括任何空白字符。
大小写敏感:
XML是区分大小写的,而HTML不区分大小写。
标记必须闭合:
在XML中,所有的标记必须有一个匹配的结束标记,而在HTML中,一些标记可以没有结
束标记。
属性值的规定:
在XML中,属性值必须分装在引号中,且所有的属性都必须带有相应的值;而在HTML
中,属性值可以不用引号括起来,可以拥有不带值的属性名。
空白字符处理:
在XML中,空白字符(如空格、换行符、制表符等)将被保留;而在HTML中,大多数
空白字符都会被过滤掉。
预定义标签和自定义标签:
XML允许用户根据需要定义自己的标签,而HTML拥有预定义的标签,不可以由用户自
己定义。
符合XML规范的HTML叫做“符合XHTML标准”。开发的网站必须通过W3C验证。
xml编写完成以后可以用浏览器来查看,如果写错了浏览器会提示。如果明明没错,浏
览器还是提示错误,则可能是文件编码问题。