打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
.NET基础拾遗(4)委托、事件、反射与特性(下)

作者:周旭龙

链接:http://www.cnblogs.com/edisonchou/p/4827578.html


2.4 如何使用事件模拟场景:猫叫->老鼠逃跑 & 主人惊醒


  这是一个典型的观察者模式的应用场景,事件的发源在于猫叫这个动作,在猫叫之后,老鼠开始逃跑,而主人则会从睡梦中惊醒。可以发现,主人和老鼠这两个类型的动作相互之间没有联系,但都是由猫叫这一事件触发的。


  设计的大致思路在于,猫类包含并维护一个猫叫的动作,主人和老鼠的对象实例需要订阅猫叫这一事件,保证猫叫这一事件发生时主人和老鼠可以执行相应的动作。


  (1)设计猫类,为其定义一个猫叫的事件CatCryEvent:


public class Cat

{

private string name;

// 猫叫的事件

public event EventHandler CatCryEvent;


public Cat(string name)

{

this.name = name;

}


// 触发猫叫事件

public void CatCry()

{

// 初始化事件参数

CatCryEventArgs args = new CatCryEventArgs(name);

Console.WriteLine(args);

// 开始触发事件

CatCryEvent(this, args);

}

}


public class CatCryEventArgs : EventArgs

{

private string catName;


public CatCryEventArgs(string catName)

: base()

{

this.catName = catName;

}


public override string ToString()

{

string message = string.Format('{0}叫了', catName);

return message;

}

}


  (2)设计老鼠类,在其构造方法中订阅猫叫事件,并提供对应的处理方法


public class Mouse

{

private string name;

// 在构造方法中订阅事件

public Mouse(string name, Cat cat)

{

this.name = name;

cat.CatCryEvent += CatCryEventHandler;

}


// 猫叫的处理方法

private void CatCryEventHandler(object sender, CatCryEventArgs e)

{

Run();

}


// 逃跑方法

private void Run()

{

Console.WriteLine('{0}逃走了:我勒个去,赶紧跑啊!', name);

}

}


  (3)设计主人类,在其构造犯法中订阅猫叫事件,并提供对应的处理方法


public class Master

{

private string name;


// 在构造方法中订阅事件

public Master(string name, Cat cat)

{

this.name = name;

cat.CatCryEvent += CatCryEventHandler;

}


// 针对猫叫的处理方法

private void CatCryEventHandler(object sender, CatCryEventArgs e)

{

WakeUp();

}


// 具体的处理方法——惊醒

private void WakeUp()

{

Console.WriteLine('{0}醒了:我勒个去,叫个锤子!', name);

}

}


  (4)最后在Main方法中进行场景的模拟:


class Program

{

static void Main(string[] args)

{

Cat cat = new Cat('假老练');

Mouse mouse1 = new Mouse('风车车', cat);

Mouse mouse2 = new Mouse('米奇妙', cat);

Master master = new Master('李扯火', cat);

// 毛开始叫了,老鼠和主人有不同的反应

cat.CatCry();


Console.ReadKey();

}

}


  这里定义了一只猫,两只老鼠与一个主人,当猫的CatCry方法被执行到时,会触发猫叫事件CatCryEvent,此时就会通知所有这一事件的订阅者。本场景的关键之处就在于主人和老鼠的动作应该完全由猫叫来触发。下面是场景模拟代码的运行结果:




三、反射基础


3.1 反射的基本原理是什么?其实现的基石又是什么?


  反射是一种动态分析程序集、模块、类型及字段等目标对象的机制,它的实现依托于元数据。元数据,就是描述数据的数据。在CLR中,元数据就是对一个模块定义或引用的所有东西的描述系统。


3.2 .NET中提供了哪些类型实现反射?


  在.NET中,为我们提供了丰富的可以用来实现反射的类型,这些类型大多数都定义在System.Reflection命名空间之下,例如Assembly、Module等。利用这些类型,我们就可以方便地动态加载程序集、模块、类型、方法和字段等元素。


  下面我们来看一个使用示例,首先是创建一个程序集SimpleAssembly,其中有一个类为SimpleClass:


[Serializable]

class SimpleClass

{

private String _MyString;

public SimpleClass(String mystring)

{

_MyString = mystring;

}


public override string ToString()

{

return _MyString;

}


static void Main(string[] args)

{

Console.WriteLine('简单程序集');

Console.Read();

}

}


  其次是对程序集中的模块进行分析,分别利用反射对程序集、模块和类进行分析:


public class AnalyseHelper

{

///

/// 分析程序集

///

public static void AnalyzeAssembly(Assembly assembly)

{

Console.WriteLine('程序集名字:' + assembly.FullName);

Console.WriteLine('程序集位置:' + assembly.Location);

Console.WriteLine('程序集是否在GAC中:' +

assembly.GlobalAssemblyCache.ToString());

Console.WriteLine('包含程序集的模块名' +

assembly.ManifestModule.Name);

Console.WriteLine('运行程序集需要的CLR版本:' +

assembly.ImageRuntimeVersion);

Console.WriteLine('现在开始分析程序集中的模块');

Module[] modules = assembly.GetModules();

foreach (Module module in modules)

{

AnalyzeModule(module);

}

}


///

/// 分析模块

///

public static void AnalyzeModule(Module module)

{

Console.WriteLine('模块名:' + module.Name);

Console.WriteLine('模块的UUID:' + module.ModuleVersionId);

Console.WriteLine('开始分析模块下的类型');

Type[] types = module.GetTypes();

foreach (Type type in types)

{

AnalyzeType(type);

}

}


///

/// 分析类型

///

public static void AnalyzeType(Type type)

{

Console.WriteLine('类型名字:' + type.Name);

Console.WriteLine('类型的类别是:' + type.Attributes);

if (type.BaseType != null)

Console.WriteLine('类型的基类是:' + type.BaseType.Name);

Console.WriteLine('类型的GUID是:' + type.GUID);

//设置感兴趣的类型成员

BindingFlags flags = (BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);

//分析成员

FieldInfo[] fields = type.GetFields(flags);

if (fields.Length > 0)

{

//Console.WriteLine('开始分析类型的成员');

foreach (FieldInfo field in fields)

{

// 分析成员

}

}

//分析包含的方法

MethodInfo[] methods = type.GetMethods(flags);

if (methods.Length > 0)

{

//Console.WriteLine('开始分析类型的方法');

foreach (MethodInfo method in methods)

{

// 分析方法

}

}

//分析属性

PropertyInfo[] properties = type.GetProperties(flags);

if (properties.Length > 0)

{

//Console.WriteLine('开始分析类型的属性');

foreach (PropertyInfo property in properties)

{

// 分析属性

}

}

}

}


  最后编写入口方法来尝试分析一个具体的程序集:


[PermissionSetAttribute(SecurityAction.Demand, Name = 'FullTrust')]

class Program

{

static void Main(string[] args)

{

Assembly assembly = Assembly.LoadFrom(@'..\..\..\SimpleAssembly\bin\Debug\SimpleAssembly.exe');

AnalyseHelper.AnalyzeAssembly(assembly);


// 创建一个程序集中的类型的对象

Console.WriteLine('利用反射创建对象');

string[] paras = { '测试一下反射效果' };

object obj = assembly.CreateInstance(assembly.GetModules()[0].GetTypes()[0].ToString(), true, BindingFlags.CreateInstance, null, paras, null, null);

Console.WriteLine(obj);


Console.ReadKey();

}

}


  上面的代码按照 程序集->模块->类型 三个层次的顺序来动态分析一个程序集,当然还可以继续递归类型内部的成员,最后通过CreateInstance方法来动态创建了一个类型,这些都是反射经常被用来完成的功能,执行结果如下图所示:




3.3 如何使用反射实现工厂模式?


  工厂模式是一种比较常用的设计模式,其基本思想在于使用不同的工厂类型来打造不同产品的部件。例如,我们在打造一间屋子时,可能需要窗户、屋顶、门、房梁、柱子等零部件。有的屋子需要很多根柱子,而有的屋子又不需要窗户。在这样的需求下,就可以使用工厂模式。


  (1)工厂模式的传统实现和其弊端


  下图展示了针对屋子设计的传统工厂模式架构图:




  上图的设计思路是:


  ①使用者告诉工厂管理者需要哪个产品部件;


  ②工厂管理者分析使用者传入的信息,生成合适的实现工厂接口的类型对象;


  ③通过工厂生产出相应的产品,返回给使用者一个实现了该产品接口的类型对象;


  通过上述思路,实现代码如下:


  ①首先是定义工厂接口,产品接口与产品类型的枚举


///

/// 屋子产品的零件

///

public enum RoomParts

{

Roof,

Window,

Pillar

}


///

/// 工厂接口

///

public interface IFactory

{

IProduct Produce();

}


///

/// 产品接口

///

public interface IProduct

{

string GetName();

}


  ②其次是具体实现产品接口的产品类:窗户、屋顶和柱子


///

/// 屋顶

///

public class Roof : IProduct

{

// 实现接口,返回产品名字

public string GetName()

{

return '屋顶';

}

}


///

/// 窗户

///

public class Window : IProduct

{

// 实现接口,返回产品名字

public string GetName()

{

return '窗户';

}

}


///

/// 柱子

///

public class Pillar : IProduct

{

// 实现接口,返回产品名字

public string GetName()

{

return '柱子';

}

}


  ③然后是具体实现工厂接口的工厂类:实现接口返回一个具体的产品对象


///

/// 屋顶工厂

///

public class RoofFactory : IFactory

{

// 实现接口,返回一个产品对象

public IProduct Produce()

{

return new Roof();

}

}


///

/// 窗户工厂

///

public class WindowFactory : IFactory

{

// 实现接口,返回一个产品对象

public IProduct Produce()

{

return new Window();

}

}


///

/// 柱子工厂

///

public class PillarFactory : IFactory

{

// 实现接口,返回一个产品对象

public IProduct Produce()

{

return new Pillar();

}

}


  ④最后是工厂管理类:组织起众多的产品与工厂


///

/// 工厂管理者

///

public class FactoryManager

{

public static IProduct GetProduct(RoomParts part)

{

IFactory factory = null;

// 传统工厂模式的弊端:工厂管理类和工厂类族的紧耦合

switch (part)

{

case RoomParts.Roof:

factory = new RoofFactory();

break;

case RoomParts.Window:

factory = new WindowFactory();

break;

case RoomParts.Pillar:

factory = new PillarFactory();

break;

default:

return null;

}


// 利用工厂生产产品

IProduct product = factory.Produce();

Console.WriteLine('生产了一个产品:{0}', product.GetName());


return product;

}

}


  按照国际惯例,我们实现一个入口方法来测试一下:


class Customer

{

static void Main(string[] args)

{

// 根据需要获得不同的产品零件

IProduct window = FactoryManager.GetProduct(RoomParts.Window);

Console.WriteLine('我获取到了{0}',window.GetName());

IProduct roof = FactoryManager.GetProduct(RoomParts.Roof);

Console.WriteLine('我获取到了{0}', roof.GetName());

IProduct pillar = FactoryManager.GetProduct(RoomParts.Pillar);

Console.WriteLine('我获取到了{0}', pillar.GetName());


Console.ReadKey();

}

}


  在Customer类中,我们通过工厂管理类根据需要的不同零件类型获取到了不同的产品零件,其运行结果如下图所示:




  当一个新的产品—地板需要被添加时,我们需要改的地方是:添加零件枚举记录、添加针对地板的工厂类、添加新地板产品类,修改工厂管理类(在switch中添加一条case语句),这样设计的优点在于无论添加何种零件,产品使用者都不需要关心内部的变动,可以一如既往地使用工厂管理类来得到希望的零件,而缺点也有以下几点:


  ①工厂管理类和工厂类族耦合;


  ②每次添加新的零件都需要添加一对工厂类和产品类,类型会越来越多;


  (2)基于反射的工厂模式的实现


  利用反射机制可以实现更加灵活的工厂模式,这一点体现在利用反射可以动态地获知一个产品由哪些零部件组成,而不再需要用一个switch语句来逐一地寻找合适的工厂。


  ①产品、枚举和以上一致,这里的改变主要在于添加了两个自定义的特性,这两个特性会被分别附加在产品类型和产品接口上:


///

/// 该特性用于附加在产品类型之上

///

[AttributeUsage(AttributeTargets.Class)]

public class ProductAttribute : Attribute

{

// 标注零件的成员

private RoomParts myRoomPart;


public ProductAttribute(RoomParts part)

{

myRoomPart = part;

}


public RoomParts RoomPart

{

get

{

return myRoomPart;

}

}

}


///

/// 该特性用于附加在产品接口类型之上

///

[AttributeUsage(AttributeTargets.Interface)]

public class ProductListAttribute : Attribute

{

// 产品类型集合

private Type[] myList;


public ProductListAttribute(Type[] products)

{

myList = products;

}


public Type[] ProductList

{

get

{

return myList;

}

}

}


  ②下面是产品接口和产品类族的定义,其中产品接口使用了ProductListAttribute特性,而每个产品都使用了ProductAttribute特性:


///

/// 产品接口

///

[ProductList(new Type[] { typeof(Roof), typeof(Window), typeof(Pillar) })]

public interface IProduct

{

string GetName();

}


///

/// 屋顶

///

[Product(RoomParts.Roof)]

public class Roof : IProduct

{

// 实现接口,返回产品名字

public string GetName()

{

return '小天鹅屋顶';

}

}


///

/// 窗户

///

[Product(RoomParts.Window)]

public class Window : IProduct

{

// 实现接口,返回产品名字

public string GetName()

{

return '双汇窗户';

}

}


///

/// 柱子

///

[Product(RoomParts.Pillar)]

public class Pillar : IProduct

{

// 实现接口,返回产品名字

public string GetName()

{

return '小米柱子';

}

}


  ③下面是修改后的工厂类,由于使用了反射特性,这里一个工厂类型就可以生产所有的产品:


///

/// 工厂类

///

public class Factory

{

public IProduct Produce(RoomParts part)

{

// 通过反射从IProduct接口中获得属性从而获得所有产品列表

ProductListAttribute attr = (ProductListAttribute)Attribute.GetCustomAttribute(typeof(IProduct), typeof(ProductListAttribute));

// 遍历所有的实现产品零件类型

foreach (var type in attr.ProductList)

{

// 利用反射查找其属性

ProductAttribute pa = (ProductAttribute)Attribute.GetCustomAttribute(type, typeof(ProductAttribute));

// 确定是否是需要到的零件

if(pa.RoomPart == part)

{

// 利用反射动态创建产品零件类型实例

object product = Assembly.GetExecutingAssembly().CreateInstance(type.FullName);

return product as IProduct;

}

}


return null;

}

}


  ④最后时修改后的工厂管理类,核心只有三行代码:


///

/// 工厂管理者

///

public class FactoryManager

{

public static IProduct GetProduct(RoomParts part)

{

// 一共只有一个工厂

Factory factory = new Factory();

IProduct product = factory.Produce(part);

Console.WriteLine('生产了一个产品:{0}', product.GetName());

return product;

}

}


  上述代码中最主要的变化在于两点:其一是工厂管理类不再需要根据不同的零件寻找不同的工厂,因为只有一个工厂负责处理所有的产品零件;其二是产品类型和产品接口应用了两个自定义特性,来方便工厂进行反射。ProductAttribute附加在产品类上,标注了当前类型代表了哪个产品零件。而ProductListAttribute则附加在产品接口之上,方便反射得知一共有多少产品零件。


  这时需要添加一个新的地板产品零件类型时,我们需要做的是:添加零件枚举记录,添加代表地板的类型,修改添加在IProduct上的属性初始化参数(增加地板类型),可以看到这时调用者、工厂管理类和工厂都不再需要对新添加的零件进行改动,程序只需要添加必要的类型和枚举记录即可。当然,这样的设计也存在一定缺陷:反射的运行效率相对较低,在产品零件相对较多时,每生产一个产品就需要反射遍历这是一件相当耗时的工作。


四、特性基础


  特性机制可以帮助程序员以申明的方式进行编程,而不再需要考虑实现的细节。


4.1 神马是特性?如何自定义一个特性?


  (1)特性是什么?


  特性是一种有别于普通命令式编程的编程方式,通常被称为申明式编程方式。所谓申明式编程方式就是指程序员只需要申明某个模块会有怎样的特性,而无需关心如何去实现。下面的代码就是特性在ASP.NET MVC中的基本使用方式:


[HttpPost]

public ActionResult Add(UserInfo userInfo)

{

if (ModelState.IsValid)

{

// To do fun

}


return RedirectToAction('Index');

}


  当一个特性被添加到某个元素上时,该元素就被认为具有了这个特性所代表的功能或性质,例如上述代码中Add方法在添加了HttpPost特性之后,就被认为只有遇到以POST的方式请求该方法时才会被执行。


Note:特性在被编译器编译时,和传统的命令式代码不同,它会被以二进制数据的方式写入模块文件的元数据之中,而在运行时再被解读使用。特性也是经常被反射机制应用的元素,因为它本身是以元数据的形式存放的。


  (2)如何自定义特性


  除了直接使用.NET中内建的所有特性之外,我们也可以建立自己的特性来实现业务逻辑。在上面反射工厂的实现中就使用到了自定义特性。具体来说,定义一个特性的本质就是定义一个继承自System.Attribute类的类型,这样的类型就被编译器认为是一个特性类型。


  下面我们看看如何自顶一个特性并使用该特性:


  ①定义一个继承自System.Attribute的类型MyCustomAttribute


///

/// 一个自定义特性MyCustomAttribute

///

[AttributeUsage(AttributeTargets.Class)]

public class MyCustomAttribute : Attribute

{

private string className;


public MyCustomAttribute(string className)

{

this.className = className;

}


// 一个只读属性ClassName

public string ClassName

{

get

{

return className;

}

}

}


  一个继承自System.Attribute的类型,就是一个自定义特性,并且可以将其添加到适合的元素之上。特性将会被写入到元数据之中,所以特性的使用基本都是基于反射机制。


  ②在入口方法中使用MyCustomAttribute


[MyCustom('UseMyCustomAttribute')]

class UseMyCustomAttribute

{

static void Main(string[] args)

{

Type t = typeof(UseMyCustomAttribute);

// 通过GetCustomAttributes方法得到自定义特性

object[] attrs = t.GetCustomAttributes(false);

MyCustomAttribute att = attrs[0] as MyCustomAttribute;


Console.WriteLine(att.ClassName);

Console.ReadKey();

}

}


  为入口方法所在的类型UseMyCustomAttribute类添加了一个自定义特性,就可以在该类的方法中通过调用该类型的GetCustomAttributes方法获取所有添加到该类型的自定义特性数组,也就可以方便使用该自定义特性所具备的性质和能力(例如代码中的属性成员可以方便获取)。


关于自定义特性,有几点需要注意:


  • 虽然没有强制规定,但按照约定最好特性类型的名字都以Attribute结尾;

  • 在C#中为了方便起见,使用特性时都可以省略特性名字后的Attribute,例如上述代码中的[MyCustom('UseMyCustomAttribute')]代替了[MyCustomAttribute('UseMyCustomAttribute')];

  • 特性类型自身也可以添加其他的特性;


4.2 .NET中特性可以在哪些元素上使用?


  特性可以被用来使用到某个元素之上,这个元素可以是字段,也可以是类型。对于类、结构等元素,特性的使用可以添加在其定义的上方,而对于程序集、模块等元素的特性来说,则需要显式地告诉编译器这些特性的作用目标。例如,在C#中,通过目标关键字加冒号来告诉编译器的使用目标:


// 应用在程序集

[assembly:MyCustomAttribute]

// 应用在模块

[module: MyCustomAttribute]

// 应用在类型

[type: MyCustomAttribute]

  

我们在设计自定义特性时,往往都具有明确的针对性,例如该特性只针对类型、接口或者程序集,限制特性的使用目标可以有效地传递设计者的意图,并且可以避免不必要的错误使用特性而导致的元数据膨胀。AttributeUsage特性就是用来限制特性使用目标元素的,它接受一个AttributeTargets的枚举对象作为输入来告诉AttributeUsage西望望对特性做何种限定。例如上面展示的一个自定义特性,使用了限制范围:


[AttributeUsage(AttributeTargets.Class)]

public class MyCustomAttribute : Attribute

{

.....

}


Note:一般情况下,自定义特性都会被限制适用范围,我们也应该养成这样的习惯,为自己设计的特性加上AttributeUsage特性,很少会出现使用在所有元素上的特性。即便是可以使用在所有元素上,也应该显式地申明[AttributeUsage(AttributesTargets.All)]来提高代码的可读性。


4.3 如何获知一个元素是否申明了某个特性?


  在.NET中提供了很多的方法来查询一个元素是否申明了某个特性,每个方法都有不同的使用场合,但是万变不离其宗,都是基于反射机制来实现的。


  首先,还是以上面的MyCustomAttribute特性为例,新建一个入口方法类Program:


///

/// 一个自定义特性MyCustomAttribute

///

[AttributeUsage(AttributeTargets.Class)]

public class MyCustomAttribute : Attribute

{

private string className;


public MyCustomAttribute(string className)

{

this.className = className;

}


// 一个只读属性ClassName

public string ClassName

{

get

{

return className;

}

}

}


[MyCustom('Program')]

class Program

{

static void Main(string[] args)

{

Type attributeType = typeof(MyCustomAttribute);

Type thisClass = typeof(Program);


}

}


  (1)System.Attribute.IsDefined方法


// 使用IsDefined方法

bool isDefined = Attribute.IsDefined(thisClass, attributeType);

Console.WriteLine('Program类是否申明了MyCustomAttribute特性:{0}', isDefined);

  

(2)System.Attribute.GetCustomerAttribute方法


// 使用Attribute.GetCustomAttribute方法

Attribute att = Attribute.GetCustomAttribute(thisClass, attributeType);

if (att != null)

{

Console.WriteLine('Program类申明了MyCustomAttribute特性,特性的成员为:{0}', (att as MyCustomAttribute).ClassName);

}


  (3)System.Attribute.GetCustomerAttributes方法


// 使用Attribute.GetCustomAttributes方法

Attribute[] atts = Attribute.GetCustomAttributes(thisClass, attributeType);

if (atts.Length > 0)

{

Console.WriteLine('Program类申明了MyCustomAttribute特性,特性名称为:{0}', ((MyCustomAttribute)atts[0]).ClassName);

}


  (4)System.Reflection.CustomAttributeData类型


// 使用CustomAttributeData.GetCustomAttributes方法

IList attList = CustomAttributeData.GetCustomAttributes(thisClass);

if (attList.Count > 0)

{

Console.WriteLine('Program类申明了MyCustomAttribute特性');

// 注意:这里可以对特性进行分析,但无法得到其实例

CustomAttributeData attData = attList[0];

Console.WriteLine('该特性的名字是:{0}', attData.Constructor.DeclaringType.Name);

Console.WriteLine('该特性的构造方法有{0}个参数', attData.ConstructorArguments.Count);

}


  下图是四种方式的执行结果:




  这四种方法各有其特点,但都可以实现查询某个元素是否申明了某个特性的这一功能。其中,可以看到第(4)种方式,可以对特性进行分析,但无法得到其实例。另外,自定义特性被申明为sealed表示不可继承,这是因为在特性被检查时,无法分别制定特性和其派生特性,这一点需要我们注意。


4.4 一个元素是否可以重复申明同一个特性?


  对于有些业务逻辑来说,一个特性反复地申明在同一个元素上市没有必要的,但同时对于另一些逻辑来说,又非常有必要对同一元素多次申明同一特性。很幸运,.NET的特性机制完美支持了这一类业务需求。


  当一个特性申明了AttributeUsage特性并且显式地将AllowMultiple属性设置为true时,该特性就可以在同一元素上多次申明,否则的话编译器将报错。


  例如下面一段代码,类型Program多次申明了MyCustomAttribute特性:


[MyCustom('Class1')]

[MyCustom('Class2')]

[MyCustom('Class3')]

public class Program

{

public static void Main(string[] args)

{

}

}


///

/// 一个自定义特性MyCustomAttribute

///

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]

public class MyCustomAttribute : Attribute

{

private string className;


public MyCustomAttribute(string className)

{

this.className = className;

}


// 一个只读属性ClassName

public string ClassName

{

get

{

return className;

}

}

}


  通常情况下,重复申明同一特性往往会传入不同的参数。此外,如果不显式地设置AllowMultiple属性时多次申明同一特性会如何呢?在这种情况下,编译器将会认为自定义特性不能多次申明在同一元素上,会出现以下的编译错误:




参考资料


(1)朱毅,《进入IT企业必读的200个.NET面试题》


(2)张子阳,《.NET之美:.NET关键技术深入解析》


(3)王涛,《你必须知道的.NET》




微信号:iDotNet

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
详解C#编程中的反射机制与方法
带你了解C#每个版本新特性
最简单的C#快速入门教程
[你必须知道的.NET] 第三回:历史纠葛:特性和属性
[ASP.NET MVC 小牛之路]02 - C#知识点提要
C#创建自定义特性
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服