深入理解C#:编程技巧总结(二)

以下总结参阅了:MSDN文档、《C#高级编程》、《C#本质论》、前辈们的博客等资料,如有不正确的地方,请帮忙及时指出!以免误导!
在上一篇 深入理解C#:编程技巧总结(一) 中总结了25点,这一篇继续:
26.系列化与反系列化
  • 使用的场合:
    便于保存,把持有运行状态的对象系列化后保存到本地,在下次运行程序时,反系列化该对象来恢复状态
    便于传输,在网络中传输系列化后的对象,接收方反系列化该对象还原
    复制黏贴,复制到剪贴板,然后黏贴

  • 用来辅助系列化和反系列化的特性:在System.Runtime.Serialization命名空间下
    OnDeserialized,应用于某个方法,该方法会在反系列化后立即被自动调用(可用于处理生成的对象的成员)
    OnDeserializing,应用于某个方法,该方法会在执行反系列化时被自动调用
    OnSerialized,应用于某个方法,对象在被系列化后调用该方法
    OnSerializing,应用于某个方法,在系列化对象前调用该方法
    如果以上辅助特性仍不能满足需求,那就要为目标对象实现ISerializable接口了

  • ISerializable接口:该接口运行对象自己控制系列化与反系列化的过程(实现该接口的同时也必须应用Serializable特性)

原理:若系列化一个对象时,发现对象实现了ISerializable接口,则会忽略掉类型所有的系列化特性应用,转而调用类型的GetObjectData()接口方法,该方法会构造一个SerializationInfo对象,方法内部负责对该对象设置需要系列化的字段,然后系列化器根据该对象来系列化。反系列化时,若发现反系列化后的对象实现了ISerializable接口,则反系列化器会把数据反系列化为SerializationInfo类型的对象,然后调用匹配的构造函数来构造目标类型的对象。

系列化:需要实现GetObjectData()方法,该方法在对象被系列化时自动被调用
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context){ }

反序列化:需要为它定义一个带参数的受保护的构造函数,用于反系列化后重新构造对象
protected Person(SerializationInfo info, StreamingContext context) { }
利用该接口,可以实现将数据流反系列化为其他任意指定对象(在原对象定义GetObjectData()方法,在目标对象定义用于反系列化的构造函数,但两个对象都必须实现ISerializable接口)
注意:
若父类为实现ISerializable接口,只有子类实现ISerializable接口,若想系列化从父类继承的字段,则需要在子类的反系列化构造器中和GetObjectData()方法中,添加继承自父类的字段的处理代码
若父类也实现了ISerializable接口,则只需在子类的反系列化构造器中和GetObjectData()方法中调用父类的版本即可

public class Class1
{public static void Main(){Person person = new Person() { FirstName = "RuiFu", LastName = "Su"};//系列化person对象并存进文件中,MyBinarySerializer为自定义工具类MyBinarySerializer.SerializeToFile<Person>(person, @"c:\", "Person.txt");//从文件中取出数据反系列化为Man类型的对象Man man = MyBinarySerializer.DeserializeFromFile<Man>(@"c:\Person.txt");Console.WriteLine(man.Name);Console.ReadKey();}
}
[Serializable]
public class Man:ISerializable
{public string Name;protected Man(SerializationInfo info,StreamingContext context){Name = info.GetString("Name");}void ISerializable.GetObjectData(SerializationInfo info,StreamingContext context){ }
}
[Serializable]
public class Person:ISerializable
{public string FirstName;public string LastName;void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context){//设置反系列化后的对象类型info.SetType(typeof(Man)); //根据原对象的成员来为info对象添加字段(这些字段将被系列化)info.AddValue("Name", string.Format("{0} {1}", LastName, FirstName));}
}

不应该被系列化的成员:
为了节省空间、流量,如果一个字段反系列化后对保存状态无意义,就没必要系列化它
如果一个字段可以通过其它字段推算出来,则没必要系列化它,而用OnDeserializedAttribute特性来触发推算方法执行
对于私密信息不应该被系列化
若成员对应的类型本身未被设置为可系列化,则应该把他标注为不可系列化[NonSerialized],否则运行时会抛出SerializationException
把属性设置为不可系列化:把它的后备字段设置为不可系列化即可实现
把事件设置为不可系列化:[field:NonSerialized]

正常系列化与反系列化示例:自定义了工具类MyBinarySerializer

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
public class Class1
{public static void Main(){Person person1 = new Person() { FirstName = "RuiFu", LastName = "Su", FullName = "Su RuiFU",IDCode="0377"};//系列化person1并存进文件中MyBinarySerializer.SerializeToFile<Person>(person1, @"c:\", "Person.txt");//从文件中取出数据反系列化为对象(文件中不含FullName信息,但系列化后自动执行了预定义的推算方法)Person person2 = MyBinarySerializer.DeserializeFromFile<Person>(@"c:\Person.txt");Console.WriteLine(person2.FullName);Console.ReadKey();}
}
[Serializable]
public class Person
{public string FirstName;public string LastName;[NonSerialized] //禁止被系列化public string FullName; //可被以上2个字段推算出来[OnDeserialized] //反系列化后将被调用的方法void GetFullName(StreamingContext context){FullName = string.Format("{0} {1}", LastName, FirstName);}[NonSerialized]private string idCode;public string IDCode{get{return idCode;}set{idCode = value;}}[field: NonSerialized]public event EventHandler NameChanged;
}
//自定义的系列化与反系列化工具类
public class MyBinarySerializer
{//将类型系列化为字符串public static string Serialize<T>(T t){using(MemoryStream stream = new MemoryStream()){BinaryFormatter formatter = new BinaryFormatter();formatter.Serialize(stream, t);return System.Text.Encoding.UTF8.GetString(stream.ToArray());}}//将类型系列化为文件public static void SerializeToFile<T>(T t, string path, string fullName){if(!Directory.Exists(path)){Directory.CreateDirectory(path);}string fullPath = string.Format(@"{0}\{1}", path, fullName);using(FileStream stream = new FileStream(fullPath,FileMode.OpenOrCreate)){BinaryFormatter formatter = new BinaryFormatter();formatter.Serialize(stream, t);stream.Flush();}}//将字符串反系列化为类型public static TResult Deserialize<TResult>(string s) where TResult: class{byte[] bs = System.Text.Encoding.UTF8.GetBytes(s);using(MemoryStream stream = new MemoryStream()){BinaryFormatter formatter = new BinaryFormatter();return formatter.Deserialize(stream) as TResult;}}//将文件反系列化为类型public static TResult DeserializeFromFile<TResult>(string path) where TResult: class{using(FileStream stream = new FileStream(path,FileMode.Open)){BinaryFormatter formatter = new BinaryFormatter();return formatter.Deserialize(stream) as TResult;}}
}
27.异常处理:抛出异常是需要消耗性能的(但相对于低概率事件,这点性能影响是微不足道的)
  • 不要利用异常处理机制来实现控制流的转移

  • 不要对能预知到的大概率、可恢复的错误抛出异常,而应该用实际代码来处理可能出现的错误

  • 仅在为了防止出现小概率预知错误、无法预知的错误和无法处理的情况才尝试抛出异常(如:运行代码会造成内存泄漏、资源不可用、应用程序状态不可恢复,则需要抛出异常)

  • 若要把错误呈现给最终的用户,则应该先捕获该异常,对敏感信息进行包装后,重新抛出一个显示友好信息的新异常

  • 底层代码引发的异常对于高层代码没有意义时,则可以捕获该异常,并重新引发意思明确的异常

  • 在重新引发异常时,总是为新异常对象提供Inner Exception对象参数(不需要提供信息时最好直接用空的throw;语句,它会把原始异常对象重新抛出),该对象保存了旧异常的一切信息,包括异常调用栈,便于代码调试

  • 用异常处理代替返回错误代码的方式,因为返回错误代码不利于维护

  • 千万不要捕获在当前上下文中无法处理的异常,否则就可能制造了一个隐藏的很深的BUG

  • 避免使用多层嵌套的try...catch,嵌套多了,谁都会蒙掉

  • 对于正常的业务逻辑,使用Test-Doer模式来代替抛出异常

    private bool CheckNumber(int number, ref string message)
    {
    if(number < 0)
    {message = "number不能为负数。";return false;
    }
    else if(number > 100)
    {message = "number不能大于100。";return false;
    }
    return true;
    }
    //调用:
    string msg = string.Empty;
    if(CheckNumber(59, ref msg)
    {
    //正常逻辑处理代码
    }
  • 对于try...finally,除非在执行try块的代码时程序意外退出,否则,finally块总是会被执行

28.多线程的异常处理
  • 在线程上若有未处理的异常,则会触发进程AppDomain.UnHandledException事件,该事件会接收到未处理异常的通知从而调用在它上面注册的方法,然后应用程序退出(注册方法无法阻止应用程序的退出,我们只能利用该方法来记录日志)

  • 在Windows窗体程序中,可以用Application.ThreadException事件来处理窗体线程中所发生的未被处理的异常,用AppDomain.UnHandledException事件来处理非窗体线程中发生的未被处理的异常。ThreadException事件可以阻止应用程序的退出。

  • 正常情况下,try...catch只能捕获当前线程的异常,一个线程中的异常只能在该线程内部才能被捕获到,也就是说主线程无法直接捕获子线程中的异常,若要把线程中的异常抛给主线程处理,需要用特殊手段,我写了如下示例代码做参考:

    static Action<Exception> action;//直接用预定义的Action委托类
    static Exception exception;
    public static void Main()
    {action = CatchThreadException; //注册方法Thread t1 = new Thread(new ThreadStart(delegate{try{Console.WriteLine("子线程执行!");throw new Exception("子线程t1异常");}catch(Exception ex){OnCatchThreadException(ex); //执行方法//如果是windows窗体程序,则可以直接用如下方法://this.BeginInvoke((Action)delegate//{//    throw ex; //将在主线程引发Application.ThreadException//}}}));t1.Start();t1.Join();//等待子线程t1执行完毕后,再返回主线程执行if(exception!=null){Console.WriteLine("主线程:{0}", exception.Message);}Console.ReadKey();
    }
    static void CatchThreadException(Exception ex)
    {exception = ex;
    }
    static void OnCatchThreadException(Exception ex) //定义触发方法
    {var actionCopy = action;if(actionCopy!=null){actionCopy(ex); //触发!!!}
    }
29.自定义异常
  • 仅在有特殊需要的时候才使用自定义异常

  • 为了应对不同的业务环境,可以在底层捕获各种业务环境可能引发的异常(如使用不同的数据库类型等),然后都抛出一个共同的自定义异常给调用者,这样一来,调用者只要捕获该自定义异常类型即可

  • 让自定义异常类派生自System.Exception类或其它常见的基本异常,并让你的异常类应用[Serializable],这样就可以在需要的时候系列化异常(也可以对异常类实现ISerializable接口来自定义系列化过程)

  • 如果要对异常信息进行格式化,则需要重写Message属性
    标准自定义异常类模板:(创建自定义异常标准类的快捷方式:在VS中输入Exception后按Tab键)

    [Serializable]
    public class MyException : Exception
    {public MyException() { }public MyException(string message) : base(message) { }public MyException(string message, Exception inner) : base(message, inner) { }//用于在反系列化时构造该异常类的实例protected MyException(SerializationInfo info,StreamingContext context): base(info, context) { }  
    }
30.在CLR中方法的执行过程:
  • 首先将参数值依次存进内存栈,执行代码的过程中,会根据需要去栈中取用参数值

  • 遇到return语句时,方法返回,并把return语句的结果值存入栈顶,这个值就是最终的返回值

  • 若方法内存在finally块,则即使在try块中有return语句,最终也会在执行finlly块之后才退出方法,在这种情况下,若返回值的类型为值类型,则在finally块中对返回变量的修改将无效,方法的最终返回值都是根据return语句压入栈顶中的值(对于引用类型,返回值只是一个引用,能成功修改该引用指向的对象,但对该引用本身的修改也是无效的),如下:

    //1.值类型public static int SomeMethod(int a)
    {try{a = 10;return a;}finally{a = 100;Console.WriteLine("a={0}", a);}
    }
    //调用
    Console.WriteLine(SomeMethod(1));
    //a=100
    //10   这是方法的返回值,finally无法修改返回值
    //2.引用类型
    public class Person:ISerializable
    {
    public string FirstName;
    }
    public static Person SomeMethod(Person a)
    {try{a.FirstName = "Wang";return a;}finally{a.FirstName = "Su";a = null;Console.WriteLine("a={0}", a);}
    }
    //调用
    Person person = new Person();
    Console.WriteLine(SomeMethod(person).FirstName);  
    //a=
    //Su  finally成功修改了对象的字段,但对引用a本身的改变不影响返回值对象
31.线程池与线程的区别
  • 线程:通过System.Threading.Thread类开辟的线程,用完就自行销毁,不可重用。主要用于密集型复杂运算

  • 线程池:由System.Threading.ThreadPool类管理的一组线程,可重用。主要用于I/O等异步操作。线程池中的一条线程任务完成后,该线程不会自行销毁。相反,它会以挂起状态返回线程池。如果应用程序再次向线程池发出请求,那么这个挂起的线程将激活并执行任务,而不会创建新线程。这节约了很多开销。只要线程池中应用程序任务的排队速度低于一个线程处理每项任务的速度,那么就可以反复重用同一线程,从而在应用程序生存期内节约大量开销。

  • 线程池可以提供四种功能:异步调用方法、以一定的时间间隔调用方法、当单个内核对象得到信号通知时调用方法、当异步 I/O 请求结束时调用方法

32.多线程和异步的区别
  • 异步操作的本质:是硬件的功能,不消耗CPU资源。硬件在收到CPU的指令后,自己直接和内存交换数据,完成后会触发一个中断来通知操作完成(如:委托的BeginInvoke()方法,执行该方法时,在线程池ThreadPool中启用一条线程来处理任务,完成后会调用方法参数中指定的回掉函数,线程池中的线程是分配好的,使用时不需要new操作)

  • 线程的本质:是操作系统提供的一种逻辑功能,它是进程中一段并发运行的代码,线程需要操作系统投入CPU资源来运行和调度

  • 异步操作的优缺点:因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些 出入,而且难以调试。

  • 多线程的优缺点:多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。

  • 何时使用:当需要执行I/O操作时,应该使用异步操作。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用。而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。

32.线程同步
  • 线程同步:就是多个线程在某个对象上执行等待(等待被解锁、等待同步信号),直到该对象被解锁或收到信号。被等待的对象必须是引用类型

  • 锁定:使用关键字lock和类型Monitor(两者本质上是一样的,lock只是Monitor的语法糖),锁定一个对象并创建一段代码的块作用域,同时只允许一个线程进入该代码块,退出代码块时解锁对象,后续线程按顺序进入

  • 信号同步:涉及的类型都继承自抽象类WaitHandle,包括SemaphoreMutexEventWaitHandle(子类AutoResetEventManualResetEvent),他们的原理都一样,都是维护一个系统内核句柄。

  • EventWaitHandle维护一个由内核产生的布尔变量(阻滞状态),false表示阻塞线程,true则解除阻塞。它的子类AutoResetEvent在执行完Set()方法后会自动还原状态(每次只给一个WaitOne()方法发信号),而ManualResetEvent类在执行Set()后不会再改变状态,它的所有WaitOne()方法都能收到信号。只要WaitOne()未收到信号,它就一直阻塞当前线程,如下示例:

    public static void Main()
    {AutoResetEvent autoReset = new AutoResetEvent(false);ManualResetEvent manualReset = new ManualResetEvent(false);Thread t1 = new Thread(new ThreadStart(() =>{autoReset.WaitOne();Console.WriteLine("线程t1收到autoReset信号!");}));Thread t2 = new Thread(new ThreadStart(() =>{autoReset.WaitOne();Console.WriteLine("线程t2收到autoReset信号!");}));t1.Start();t2.Start();Thread.Sleep(1000);autoReset.Set();//t1 t2 只能有一个收到信号Thread t3 = new Thread(new ThreadStart(() =>{manualReset.WaitOne();Console.WriteLine("线程t3收到manualReset信号!");}));Thread t4 = new Thread(new ThreadStart(() =>{manualReset.WaitOne();Console.WriteLine("线程t4收到manualReset信号!");}));t3.Start();t4.Start();Thread.Sleep(1000);manualReset.Set();//t3 t4都能收到信号Console.ReadKey();
    }
34.实现c#每隔一段时间执行代码:

方法一:调用线程执行方法,在方法中实现死循环,每个循环用Thread.Sleep()设定阻塞时间(或用thread.Join());
方法二:使用System.Timers.Timer类;
方法三:使用System.Threading.Timer
具体怎么实现,就不细说了,看MSDN,或百度

 

原文地址:http://www.cnblogs.com/susufufu/p/6266216.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/325477.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

不一样的假期,到底哪里不一样?

目录&#xff1a;1.回访17级同学们&#xff0c;了解同学们目前的工作情况2.和18级同学们聊天&#xff0c;了解假期在家的学习情况3.检查19级同学们提交至小程序内的作业。回忆17级前两天将我带的17级毕业班挨个找了一遍&#xff0c;大致了解了下大家最近的工作情况&#xff0c;…

TensorFlowSharp入门使用C#编写TensorFlow人工智能应用

TensorFlowSharp入门使用C#编写TensorFlow人工智能应用学习。 TensorFlow简单介绍 TensorFlow 是谷歌的第二代机器学习系统&#xff0c;按照谷歌所说&#xff0c;在某些基准测试中&#xff0c;TensorFlow的表现比第一代的DistBelief快了2倍。 TensorFlow 内建深度学习的扩展支…

Spring Cloud 升级最新 Finchley 版本,踩了所有的坑

转载自 Spring Cloud 升级最新 Finchley 版本&#xff0c;踩了所有的坑 Spring Boot 2.x 已经发布了很久&#xff0c;现在 Spring Cloud 也发布了 基于 Spring Boot 2.x 的 Finchley 版本&#xff0c;现在一起为项目做一次整体框架升级。 升级前 > 升级后 Spring Boot …

快来看看你们的新年礼物,猜猜是什么?

春节总把新桃换旧符千门万户曈曈日春风送暖入屠苏爆竹声中一岁除新年礼物前言各位同学们&#xff0c;新春快乐哇&#xff0c;利用假期的时间&#xff0c;花费5天左右的时间&#xff0c;为大家每个人准备了一份神秘的新年礼物&#xff0c;想不想知道是什么吗&#xff1f;必看那么…

行动力决定了一个人的成败,有想法,就去做! C#的内存管理原理解析+标准Dispose模式的实现

尽管.NET运行库负责处理大部分内存管理工作&#xff0c;但C#程序员仍然必须理解内存管理的工作原理&#xff0c;了解如何高效地处理非托管的资源&#xff0c;才能在非常注重性能的系统中高效地处理内存。C#编程的一个优点就是程序员不必担心具体的内存管理&#xff0c;垃圾回收…

让面试官颤抖的 HTTP 2.0 协议面试题

转载自 让面试官颤抖的 HTTP 2.0 协议面试题 Http协议&#xff0c;对于拥有丰富开发经验的程序员来说简直是信手拈来&#xff0c;家常便饭。虽然天天见&#xff0c;但是对于http协议的问题&#xff0c;可能很多人在没有积极准备的情况下&#xff0c;不一定能很好的回答出来。…

一步步学习EF Core(3.EF Core2.0路线图)

前言 这几天一直在研究EF Core的官方文档,暂时没有发现什么比较新的和EF6.x差距比较大的东西.不过我倒是发现了EF Core的路线图更新了,下面我们就来看看 今天我们来看看最新的EF Core 2.0路线图 E文好的移步:https://github.com/aspnet/EntityFramework/wiki/Roadmap#ef-core…

Docker 核心概念、安装、端口映射及常用操作命令,详细到令人发指。

转载自 Docker 核心概念、安装、端口映射及常用操作命令&#xff0c;详细到令人发指。 Docker简介 Docker是开源应用容器引擎&#xff0c;轻量级容器技术。 基于Go语言&#xff0c;并遵循Apache2.0协议开源 Docker可以让开发者打包他们的应用以及依赖包到一个轻量级、可移…

Build Tour 2017 中国站北京、上海报名了

微软于 5 月 10 日在总部西雅图举办的 Build 2017 大会上&#xff0c;发布了针对云计算、人工智能、Windows 以及混合现实平台等技术的一系列重要更新&#xff0c;这令众多来自企业、ISV、初创企业的开发者&#xff0c;学生开发者&#xff0c;以及技术爱好者兴奋不已。 为了帮助…

getOrDefault()和subList()

返回 key 相映射的的 value&#xff0c;如果给定的 key 在映射关系中找不到&#xff0c;则返回指定的默认值。

.NET Core类库项目中如何读取appsettings.json中的配置

这是一位朋友问我的问题&#xff0c;写篇随笔回答一下。有2种方法&#xff0c;一种叫丑陋的方法 —— IConfiguration &#xff0c;一种叫优雅的方法 —— IOptions 。 1&#xff09;先看丑陋的方法 比如在 RedisClient 中需要读取 appsettings.json 中的 redis 连接字符串&a…

js引擎执行代码的基本流程

js引擎执行代码的基本流程 先执行初始化代码: 包含一些特别的代码设置定时器绑定监听发送ajax请求后面在某个时刻才会执行回调代码

微服务框架下的思维变化-OSS.Core基础思路

如今框架两字已经烂大街了&#xff0c;xx公司架构设计随处可见&#xff0c;不过大多看个热闹&#xff0c;这些框架如何来的&#xff0c;细节又是如何思考的&#xff0c;相互之间的隔离依据又是什么...相信很多朋友应该依然存在自己的疑惑&#xff0c;特别是越来越火热的微服务以…

Spring Boot 2.x 启动全过程源码分析(全)

转载自 Spring Boot 2.x 启动全过程源码分析&#xff08;全&#xff09; 上篇《Spring Boot 2.x 启动全过程源码分析&#xff08;一&#xff09;入口类剖析》我们分析了 Spring Boot 入口类 SpringApplication 的源码&#xff0c;并知道了其构造原理&#xff0c;这篇我们继…

Vue 2017 现状与展望 | 视频+PPT+速记快速回顾

微软Typescript团队和VS Code团队亲自给Vue开发插件&#xff0c;下一个版本的Vue 2.4将由微软提供支持Vue使用Typescript&#xff0c;之前为VS Code写vue扩展插件的人已入职微软VS Code团队 讲师 | 尤雨溪 速记 | kalasoo 5 月 20 日&#xff0c;在全球首届 VueConf 上&#xf…

6 道 BATJ 必考的 Java 面试题

转载自 6 道 BATJ 必考的 Java 面试题 题目一 请对比 Exception 和 Error&#xff0c;另外&#xff0c;运行时异常与一般异常有什么区别&#xff1f; 考点分析&#xff1a; 分析 Exception 和 Error 的区别&#xff0c;是从概念角度考察了 Java 处理机制。总的来说&#…

终于知道什么情况下需要实现.NET Core中的IOptions接口

自从接触 IOptions 之后&#xff0c;一直纠结这样的问题&#xff1a;自己定义的 Options 要不要实现 IOptions 接口。 微软有的项目中实现了&#xff0c;比如 Caching 中的 MemoryCacheOptions &#xff1a; public class MemoryCacheOptions : IOptions<MemoryCacheOptio…

Amazing ASP.NET Core 2.0

前言 ASP.NET Core 的变化和发展速度是飞快的&#xff0c;当你发现你还没有掌握 ASP.NET Core 1.0 的时候&#xff0c; 2.0 已经快要发布了&#xff0c;目前 2.0 处于 Preview 1 版本&#xff0c;意味着功能已经基本确定&#xff0c;还没有学习过 ASP.NET Core 的同学可以直接…