• 【C# Programming】异常处理、泛型


    一、异常处理

    1.1 多异常类型

            C# 允许代码引发从System.Exception 派生。 例如:

    1. public sealed class TextNumberParser
    2. {
    3. public static int Parse(string textDigit)
    4.     {
    5.     string[] digitTexts =  { "zero", "one", "two", "three", "four",   "five", "six", "seven", "eight", "nine" };
    6.         int result = Array.IndexOf(digitTexts, textDigit.ToLower());
    7.         if(result < 0)
    8.         throw new ArgumentException(  "The argument did not represent a digit", nameof(textDigit));
    9.         return result;
    10.     }
    11. }

            两个类似的异常是ArgumentNullException 和 NullReferenceException. 一般在解引用null值时,底层触发NullReferenceException。

            参数异常类型(ArgumentException、ArgumentNullException和ArgumentOutRangeException) 一个重要特征是,每个异常都有一个构造器参数,允许实参名标识为一个字符串. C#的规范是, 对于参数类型异常中的参数名称应该使用nameof 操作符。

            不要引发System.SystemException 和它派生的异常类型 System.stackoverflowException, System.OutOfMemoryException, System.Runtime.InteropServices.COMException,     System.ExecutionEngineException 和System. Runtime.InteropServices.SEHException

            不要引发System.Exception 或者System.ApplicationException

            考虑在程序继续执行会变得不安全时调用System.Environment.FailFast()来终止进程。

    1.2 捕获异常

            C# 允许使用多个catch 块, 每个catch 块都能定位特定的异常类型。

    1. public static void Main(string[] args)
    2. {
    3. try
    4. {
    5.     // throw new Win32Exception(42);   
    6. throw new InvalidOperationException("Arbitrary exception");
    7.     }
    8.     catch(Win32Exception exception
    9.     when(args.Length == exception.NativeErrorCode)
    10.     {
    11. //....
    12.     }
    13.     catch(NullReferenceException exception) {
    14.         // Handle NullReferenceException
    15.     }
    16.     catch(ArgumentException exception) {
    17.         // Handle ArgumentException
    18.     }
    19.     catch(InvalidOperationException exception) {
    20.         // Handle ApplicationException
    21.     }
    22.     catch(Exception exception) {
    23.         //
    24.     }
    25.     finally {
    26.         // Handle any cleanup code here as it runs  regardless of whether there is an exception
    27. }
    28. }

            重新引发已存在的异常:如果引发一个特定的异常, 会更新所有的栈信息来匹配新的引发位置。这会造成指示异常最初发生的调用位置所有栈信息丢失。 因此C#只能在catch 语句中重新抛出异常,例如:

    1. public static void Main(string[] args)
    2. {
    3. //….
    4.     catch(InvalidOperationException exception)
    5. {
    6.     bool exceptionHandled = false ;
    7. if (!exceptionHandled )
    8. throw;
    9.     }
    10.     //……
    11. }

            引发现有异常而不替换栈信息:C# 5.0  新增了一种机制,允许引发从前引发的异常而不丢失原始异常中的栈跟踪信息。 这样即使在catch 外也能重新引发异常。System.Runtime.ExceptionServices.ExceptionDispatchInfo 处理这种情况, 例如:

    1. try{
    2. //....
    3. }
    4. catch(AggregateException exception){
    5. exception = exeption.Flatten();
    6. ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
    7. }

    1.3 常规catch 块

            从c# 2.0 开始,所有异常(无论是否从System.Exception) 在进入程序集中,都会被包装成从System.Exception 派生。 

            C# 还支持常规catch 块, 即catch {}. 它在行为上和catch(System.Exception exception) 块完全一致,只是没有类型名和变量名。除此之外, 常规catch块必须是所有catch 块的最后一个。

    1.4 异常处理的规范

    异常处理规范:

    • 只捕获能处理的异常  
    • 不要隐藏未处理异常  
    • 尽可能少使用System.Exception 和常规catch 块  
    • 避免在调用栈较低位置记录或报告异常  
    • 在catch 块中使用throw,而不是 throw <异常对象> 语句  
    • 重新引发不同异常时要谨慎

    1.5 自定义异常

            自定义异常的唯一要求是从System.Exception或者它的子类派生。例如:

    1. class DatabseException: System.Expection {
    2. public DatabaseException()
    3. {
    4. //…..
    5. }
    6. public DatabaseException(string message )
    7. {
    8. //…..
    9. }
    10. public DatabaseException(string message, Exception innerException )
    11. {
    12. InnerException= innerException;
    13. //…..
    14. }
    15. public DatabaseException( System.Data.OracleclientException exception )
    16. {
    17. InnerException= innerException;
    18. //…..
    19. }
    20. }

    使用自定义异常时,应遵守以下实践:  

    • 所有异常应该使用“Exception”后缀
    • 通常,所有异常应包含以下三个构造器: 无参构造器、获取一个string 参数的构造器以及同时获取一个字符串和一个内部异常作为参数的构造器,除此之外, 也应允许任何异常数据作为构造器的一部分  
    • 避免使用深的继承类层次结构(一般应小于5级)

    二、泛型

    2.1 泛型的使用

            类似于C++,C# 中的范型类和结构要求使用尖括号声明泛型类型参数以及指定范型类型实参

    1. public void Sketch()
    2. {
    3. Stack<Cell> path = new Stack<Cell>();
    4.     Cell currentPosition;
    5.     ConsoleKeyInfo key// Added in C# 2.0
    6.     Console.WriteLine("Use arrow keys to draw. X to exit.");
    7.     for(int i = 2; i < Console.WindowHeight; i++)
    8.     Console.WriteLine();
    9.     currentPosition = new Cell(Console.WindowWidth / 2, Console.WindowHeight / 2);
    10.     path.Push(currentPosition);
    11.     FillCell(currentPosition);
    12.     do {
    13. bool bFill = false;
    14.         key = Move();
    15.         switch(key.Key) {
    16.         case ConsoleKey.Z:
    17.             if(path.Count >= 1)  
    18. {               
    19. // Undo the previous Move.
    20.             currentPosition = path.Pop();
    21.                 Console.SetCursorPosition(currentPosition.X, currentPosition.Y);
    22.                 FillCell(currentPosition, ConsoleColor.Black);
    23.                 Undo();
    24.             }
    25.             break;
    26.         case ConsoleKey.DownArrow:
    27.         if(Console.CursorTop < Console.WindowHeight - 2)
    28. currentPosition = new Cell(Console.CursorLeft, Console.CursorTop + 1);
    29.         bFill =true;
    30. break;
    31. case ConsoleKey.UpArrow:
    32.         if(Console.CursorTop > 1)
    33.             currentPosition = new Cell(Console.CursorLeft, Console.CursorTop - 1);
    34.             bFill =true;
    35.             break;
    36.        case ConsoleKey.LeftArrow:
    37.             if(Console.CursorLeft > 1)
    38.                 currentPosition = new Cell(Console.CursorLeft - 1, Console.CursorTop);
    39.             bFill =true;
    40.             break;
    41.        case ConsoleKey.RightArrow:
    42.             if(Console.CursorLeft < Console.WindowWidth - 2)
    43.                 currentPosition = new Cell(Console.CursorLeft + 1, Console.CursorTop);
    44. bFill =true;
    45.             break;
    46.        default:
    47.             Console.Beep();  // Added in C# 2.0
    48.             break;
    49.        }
    50. if (bFill){
    51. path.Push(currentPosition); // Only type Cell allowed in call to Push().
    52.            FillCell(currentPosition);
    53. }
    54. }
    55. while(key.Key != ConsoleKey.X);  // Use X to quit.
    56. }
    1. private static ConsoleKeyInfo Move() => Console.ReadKey(true);
    2. private static void Undo() {
    3.     // stub
    4. }
    5.     private static void FillCell(Cell cell)
    6.     {
    7.         FillCell(cell, ConsoleColor.White);
    8.     }
    9.     private static void FillCell(Cell cell, ConsoleColor color)
    10.    
    11.         Console.SetCursorPosition(cell.X, cell.Y);
    12.         Console.BackgroundColor = color;
    13.         Console.Write(' ');
    14.         Console.SetCursorPosition(cell.X, cell.Y);
    15.         Console.BackgroundColor = ConsoleColor.Black;
    16. }
    17. public struct Cell
    18. {
    19.     readonly public int X;
    20.     readonly public int Y;
    21.     public Cell(int x, int y)
    22.     {
    23.         X = x;
    24.         Y = y;
    25.     }
    26. }

    2.2 简单泛型类的定义

            在定义泛型类时, 仅需在类名之后使用一对尖括号指定类型参数。使用泛型类时, 类型实参将替换所有指定的类型参数。

    1. public class Stack<T>
    2. {
    3. // Use read-only field prior to C# 6.0
    4. private T[] InternalItems { get; }
    5. public void Push(T data)
    6. {
    7. //...
    8. }
    9. public T Pop()
    10. {
    11. //...
    12. return InternalItems[0];//just for the example.
    13. }
    14. }

    泛型类的优点:

    • 泛型促进了类型安全。它确保在参数化类中,只有成员明确期望的数据类型才可使用
    • 为泛型类成员使用值类型,不会造成object的装箱操作。
    • C# 泛型缓解了代码膨胀的情况
    • 性能得到了提高
    • 泛型减少了内存消耗
    • 代码的可读性更好  

    类型参数的命名规范:

            和方法参数类似,类型参数应具有可描述性,定义泛型类时参数类型名称应包含T 前缀, 例如:public class EntityCollection {           //….     }

    2.3 泛型的接口和结构

            C# 支持在语言中全面使用泛型,其中包括接口和结构。语法和类的语法完全相同。 例如:

    1. interface IPair<T>
    2. {
    3. T First { get; set; }
    4. T Second { get; set; }
    5. }

            实现接口的语法与非泛型类的语法相同,一个泛型的类型实参可以成为另一个泛型类型的类型参数。

    1. public struct Pair<T> : IPair<T>
    2. {
    3. public T First { get; set; }
    4. public T Second { get; set; }
    5. }

    2.4 在类中多次实现同一接口

            相同泛型接口的不同构造被看成是不同的类型,所以类或结构能多次实现“同一个”泛型接口。

    1. public interface IContainer<T>
    2. {
    3. ICollection<T> Items { get; set;}
    4. }
    5. public class Person : IContainer<Address>, IContainer<Phone>, IContainer<Email> {
    6. ICollection<Address> IContainer<Address>.Items {
    7. get {
    8. //...
    9. return new List<Address>();
    10. }
    11. set { //… }
    12. }
    13. ICollection<Phone> IContainer<Phone>.Items {
    14. get {
    15. //...
    16. return new List<Phone>();
    17. }
    18. set { //… }
    19. }
    20. ICollection<Email> IContainer<Email>.Items {
    21. get {
    22. //...
    23. return new List<Email>();
    24. }
    25. set { //… }
    26. }
    27. }
    28. public class Address { } // For example purposes only
    29. public class Phone { } // For example purposes only
    30. public class Email { } // For example purposes only

    2.5 构造器和析构器的定义

            泛型类或结构的构造器(析构器)不要类型参数。 例如:

    1. public struct Pair<T> : IPair<T>
    2. {
    3. public Pair(T first, T second)
    4.     {
    5.     First = first;
    6.         Second = second;
    7.     }
    8. public Pair(T first)
    9.     {
    10.     First = first;
    11.         Second = default(T); // must be initialized
    12.     }
    13.     public T First { get; set; }
    14.     public T Second { get; set; }   
    15. }

    2.6 多个类型参数

            泛型类型可以使用任意数量的类型参数。

    1. interface IPair<TFirst, TSecond>
    2. {
    3. TFirst First { get; set; }
    4.     TSecond Second { get; set; }
    5. }
    6. public struct Pair<TFirst, TSecond> : IPair<TFirst, TSecond>
    7. {
    8.     public Pair(TFirst first, TSecond second)
    9.     {
    10.     First = first;
    11.         Second = second;
    12.     }
    13.     public TFirst First { get; set; }
    14.     public TSecond Second { get; set; }
    15. }

    2.7 元组

            从C# 4.0 开始,CLR 团队定义了9 个新的泛型类型,它们都叫Tuple. 和 Pair<…> 一样, 相同名称可以重用,只要元数不同。

    1. public class Tuple
    2. { // ... }
    3. public class Tuple<T1> // : IStructuralEquatable, IStructuralComparable, IComparable
    4. { // ... }
    5. public class Tuple<T1, T2> // : IStructuralEquatable, IStructuralComparable, IComparable
    6. { // ... }
    7. public class Tuple<T1, T2, T3> // : IStructuralEquatable, IStructuralComparable, Comparable
    8. { // ... }
    9. public class Tuple<T1, T2, T3, T4> // : IStructuralEquatable, IStructuralComparable, IComparable
    10. { // ... }
    11. public class Tuple<T1, T2, T3, T4, T5> // : IStructuralEquatable, IStructuralComparable, IComparable
    12. { // ... }
    13. public class Tuple<T1, T2, T3, T4, T5, T6> // : IStructuralEquatable, IStructuralComparable, IComparable
    14. { // ... }
    15. public class Tuple<T1, T2, T3, T4, T5, T6, T7> // : IStructuralEquatable, IStructuralComparable, IComparable
    16. { // ... }
    17. public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest> // : IStructuralEquatable, IStructuralComparable, IComparable
    18. { // ... }

    2.8 嵌套泛型类型

            嵌套类型自动获得包容类型的类型参数,例如: 假如包含类型声明类型参数T,则 类型T也可以在嵌套类型中使用, 如果嵌套类型包含了自己的类型参数T,那么它会隐藏包容类型的同名类型参数。

    1. class Container<T, U>
    2. {
    3. // Nested classes inherit type parameters.
    4.     // Reusing a type parameter name will cause
    5.     // a warning.
    6.     class Nested<U>
    7.     {
    8.     void Method(T param0, U param1)
    9.         {}
    10.     }
    11. }

            在声明类型参数的类型主体的人和地方都能访问该类型参数。

    2.9 约束

            泛型允许为类型参数定义约束,这些约束强迫作为类型实参提供的类型遵守各种规则。

    1. public class BinaryTree<T> {
    2. public T Item { get; set; }
    3.     public Pair<BinaryTree<T>> SubItems
    4.     {
    5.     get { return _SubItems; }
    6.         set {
    7.         IComparable<T> first;
    8.             first = (IComparable<T>)value.First.Item;
    9.             if(first.CompareTo(value.Second.Item) < 0) {
    10.             //// first is less than second.
    11.             }
    12.             else {
    13.                 // second is less than or equal to first.
    14.             }
    15.             _SubItems = value;
    16.         }
    17.     }
    18.     private Pair<BinaryTree<T>> _SubItems;
    19. }

            如果BinaryTree 的类型参数没有实现 IComparabe 接口,则会发生执行时错误。

    2.10 接口约束

            约束描述了泛型要求的类型参数的特征。为了声明一个约束, 需要使用where 关键字, 后面跟一对参数:要求。 其中, ”参数”必须时泛型类型中声明的一个参数,而”要求” 描述了类型参数要能转换成的类或接口是否必须有默认构造器, 或者是引用类型还是值类型。

            接口约束规定了某个数据类型必须实现某个接口。 例如:

    1. public class BinaryTree<T>
    2. where T : System.IComparable<T>
    3. {
    4. public T Item { get; set; }
    5.     public Pair<BinaryTree<T>> SubItems
    6.     {
    7.     get { return _SubItems; }
    8.         set {
    9.         IComparable<T> first = value.First.Item; // Notice that the cast can now be eliminated.
    10.             if(first.CompareTo(value.Second.Item) < 0) {
    11.             //// first is less than second.
    12.             }
    13.             else {
    14.                 //... // second is less than or equal to first.
    15.            }
    16.             _SubItems = value;
    17.         }
    18.     }
    19.     private Pair<BinaryTree<T>> _SubItems;
    20. }

    2.11 类类型约束

            有时可能要求将类型实参转换为特定的类类型,这是通过类类型做到的。 例如:

    1. public class EntityDictionary<TKey, TValue>
    2. : System.Collections.Generic.Dictionary<TKey, TValue>
    3. where TValue : EntityBase
    4. {
    5. //...
    6. }
    7. public class EntityBase
    8. {}

            如果同时指定了多个约束,那么类类型约束必须第一个出现。同一个参数的多个类类型约束是不被允许的。类似的, 类类型约束不能指定密封类或者不是类的类型。

    2.12 struct/class 约束

            另一个重要的约束是将类型参数限制为任何非可空的值类型或者任何引用类型 。 编译器不允许在约束中将System.ValueType指定为基类。但是C# 提供了关键字struct/class指定参数类型是值类型还是引用类型。

    1. public struct Nullable<T> :
    2. IFormattable, IComparable,
    3. IComparable<Nullable<T>>, INullable
    4. where T : struct
    5. {
    6. // ...
    7. }

            由于类类型约束要求指定特定的类哦, 所以类类型和struct/class 约束一起使用会相互矛盾, 因此,struct/class 约束和类类型约束不能一起使用。

            struct约束有个特别的地方, 可空值类型不符合此约束,应为可空值类型是从Nullable派生的,后者已经对T应用了struct 约束。

    2.13 多个约束

            对于任何给定的类型参数,都可以指定任意数量的接口约束,但是类类型约束只能是一个。每个约束都在一个以逗号分隔的列表中声明,每个参数类型前都需要使用where关键字。

    1. public class EntityDictionary<TKey, TValue>
    2. : Dictionary<TKey, TValue>
    3. where TKey : IComparable<TKey>, IFormattable
    4. where TValue : EntityBase
    5. {
    6. // ...
    7. }

    2.14 构造器约束

            某些情况下,需要在泛型类中创建类型实参的实例。但是并非所有的对象都保证有公共默认构造器,所以编译器不允许为未约束的类型调用默认构造器。

            为了克服这一限制,类在指定了其他约束后使用new().这就是所谓的构造器约束。 它要求实参类型必须有默认构造器。只能对默认构造器约束,不能为带参数的构造器指定约束。

    1. public class EntityBase<TKey>
    2. {
    3. public TKey Key { get; set; }
    4. }
    5. public class EntityDictionary<TKey, TValue> :
    6. Dictionary<TKey, TValue>
    7. where TKey : IComparable<TKey>, IFormattable
    8. where TValue : EntityBase<TKey>, new()
    9. {
    10. // ...
    11. public TValue MakeValue(TKey key)
    12. {
    13. TValue newEntity = new TValue();
    14. newEntity.Key = key;
    15. Add(newEntity.Key, newEntity);
    16. return newEntity;
    17. }
    18. // ...
    19. }

    2.15 约束继承

            无论是泛型类型参数还是它们的约束,都不会被派生类继承,因为泛型类型参数不是成员。

            由于派生的泛型类型参数现在是泛型基类的类型实参,所以类型参数必须具有与基类相同(或更强)的约束。

    1. class EntityBase<T> where T : IComparable<T>
    2. {
    3. // ...
    4. }
    5. // ERROR:
    6. // The type 'T' must be convertible to 'System.IComparable'
    7. // to use it as parameter 'T' in the generic type or
    8. // method.
    9. // class Entity<T> : EntityBase<T>
    10. // {
    11. // ...
    12. // }

    2.16 约束限制

    约束的限制:    

    • 不能合并使用类类型约束和struct/class 约束。
    • 不能限制从一些特殊类继承。 例如  object、 数组、 System.ValueType、 System.Enum、System.Delegate以及System.MulticastDelegate  
    • 不支持操作符约束 。           
    • 不支持用约束类型参数来限制类型必须实现特定的方法或操作符。只能通过类类型约束(限制方法和操作符)和接口约束(限制方法)来提供不完整支持。
    1. public abstract class MathEx<T>
    2. {
    3. public static T Add(T first, T second)
    4. {
    5. // Error: Operator '+' cannot be applied to
    6. // operands of type 'T' and 'T'.
    7. // return first + second;
    8. return default(T);
    9. }
    10. }
    • 不支持OR 条件 假如为一个类型参数提供多个接口约束,编译器认为不同约束之间总是存在一个AND关系。不能在约束之间指定OR关系。
    1. public class BinaryTree<T>
    2. // Error: OR is not supported.
    3. //where T: System.IComparable<T> || System.IFormattable
    4. {
    5. // ...
    6. }
    • 委托和枚举类型的约束是无效的 委托类型,数组类型和枚举类型不能在基类中使用,因为他们实际是”密封”的。
    1. // Error: Constraint cannot be special class 'System.Delegate'
    2. //public class Publisher<T>
    3. // where T : System.Delegate{
    4. // public event T Event;
    5. // public void Publish()
    6. // {
    7. // if(Event != null)
    8. // Event(this, new EventArgs());
    9. // }
    10. //}
    • 构造器约束只针对默认构造器。 为了克服该限制,一个方法是提供一个工厂接口,让他包含一个方法对类型进行实例化,实现接口的工厂负责实例化实体。
    1. public class EntityBase<TKey>
    2. {
    3. public EntityBase(TKey key)
    4. {
    5. Key = key;
    6. }
    7. public TKey Key { get; set; }
    8. }
    9. public interface IEntityFactory<TKey, TValue>
    10. {
    11. TValue CreateNew(TKey key);
    12. }
    13. public class EntityDictionary<TKey, TValue, TFactory> :
    14. Dictionary<TKey, TValue>
    15. where TKey : IComparable<TKey>, IFormattable
    16. where TValue : EntityBase<TKey>
    17. where TFactory : IEntityFactory<TKey, TValue>, new()
    18. {
    19. public TValue New(TKey key)
    20. {
    21. TFactory factory = new TFactory();
    22. TValue newEntity = factory.CreateNew(key);
    23. Add(newEntity.Key, newEntity);
    24. return newEntity;
    25. }
    26. //...
    27. }
    1. public class Order : EntityBase<Guid>
    2. {
    3. public Order(Guid key) :
    4. base(key)
    5. {
    6. // ...
    7. }
    8. }
    9. public class OrderFactory : IEntityFactory<Guid, Order>
    10. {
    11. public Order CreateNew(Guid key)
    12. {
    13. return new Order(key);
    14. }
    15. }

    2.17 泛型方法

            泛型方法要使用泛型类型参数,这一点和泛型类型一致。在泛型和非泛型类型中都可以使用泛型方法。 要使用泛型方法,要在方法名后添加类型参数,例如:

    1. public static class MathEx
    2. {
    3. public static T Max<T>(T first, params T[] values)
    4. where T : IComparable<T>
    5. {
    6. T maximum = first;
    7. foreach(T item in values)
    8. {
    9. if(item.CompareTo(maximum) > 0)
    10. {
    11. maximum = item;
    12. }
    13. }
    14. return maximum;
    15. }
    16. // …..
    17. }

    2.18 泛型方法类型推断

            调用泛型方法时,要在方法的类型名后提供类型实参, 例如:

    1. public static void Main()
    2. {
    3. Console.WriteLine(
    4.     MathEx.Max<int>(7, 490));
    5.         //….
    6. }

            在大多数情况下,可以在调用时不指定类型实参。这就是所谓的类型推断。方法类型推断算法在进行推断时,只考虑方法实参、实参类型以及形参类型。

    2.20 约束的指定

            泛型方法的类型参数也允许指定约束,其方式与在泛型类型中指定类型参数方式相同,例如:

    1. public class ConsoleTreeControl {
    2. public static void Show<T>(BinaryTree<T> tree, int indent)
    3.     where T : IComparable<T>
    4.   {
    5.     Console.WriteLine("\n{0}{1}", "+ --".PadLeft(5 * indent, ' '), tree.Item.ToString());
    6.        if(tree.SubItems.First != null)
    7.         Show(tree.SubItems.First, indent + 1);
    8.        if(tree.SubItems.Second != null)
    9.            Show(tree.SubItems.Second, indent + 1);
    10.     }
    11. }

            由于BinaryTree 为类型T施加了约束,而 Show 使用了BinaryTree, 所以Show 需要施加约束。

    2.21 泛型的实例化

            基于值类型的泛型实例化:用值类型作为类型参数首次构造一个泛型类型时,”运行时” 会将指定类型参数放到CIL的合适位置,从而创建一个具体化的泛型类型。总之,”运行时” 会针对每个新的” 参数值类型“ 创建一个新的具体化的泛型类型。

            基于引用类型的泛型实例化:使用引用类型作为类型参数首次构造一个泛型类型时,运行时会在CIL代码中用object 替换参数类型创建一个具体化的泛型类型(而不是基于所提供的类型参数创建一个具体化的泛型类型). 以后,每次用引用类型参数实例化一个构造好的类型,运行时都会重用以前生成好的泛型类型的版本。

  • 相关阅读:
    前端工程化精讲第十课 流程分解:Webpack 的完整构建流程
    Set与二分法效率
    pycharm如何优雅的添加gitignore以及查看不同文件状态对应的颜色
    Tomcat常见报错以及手动实现Tomcat
    利用网络管理解决方案简化网络运维
    【HTML】
    STM32物联网基于ZigBee智能家居控制系统
    RUST 每日一省:全局变量
    ArrayLis集合扩容机制
    如何安装Vue
  • 原文地址:https://blog.csdn.net/weixin_44906102/article/details/133716543