Dean的周学习内容(24.4.1~24.4.7):C# 中高级语法

简介:

        if else的使用啦~ for循环的使用这些基本的就不再赘述了,这里只列举了我认为,稍微深入一点的内容,这些内容是我综合C# 教程 | 菜鸟教程 (runoob.com)C# 入门课程_编程实战微课_w3cschool这两部分的内容之后整理的,当然随着后面自己学习的深入,可能还会更新一下更高级的东西,比如事件了,多线程之类的,但是目前就只写一些介于 If、else、try、catch 之上,的内容,包括反射,委托、事件这些高级功能在内(可能不够精进,换欢迎大家批评指正),

ok,废话不多说,开整!!

中级知识

 1、封装:访问权限修饰符

                

由外而内来一一解释:

在讲之前,我们现需要分清楚几个概念:在VS中,程序的范围从大到小排序分别是:解决方案--->项目--->命名空间---->类,

Public:所有的类都可以方位,该修饰符限定的内容。当前解决方案里面所有的类都可以访问,不在同一个项目、命名空间都没关系。都可以访问资源,甚至其他解决方案里面的项目,引用了之后,也可以访问该资源。

Internal:只允许在同一个解决方案里面的才可以访问

Protected:只允许该类内部和该类的子类访问

Private:只有该类的内部成员可以访问

Protected Internal:并集,满足Protected、Internal任何一个条件都可以访问

他们的范围:private < internal/protected < protected internal < public

2、可空类型

这个地方好像只有C#有?因为我之前写Java的时候没有用到过好像,emmm,也懒得没去查,有懂的小伙伴可以评论或者私信告诉我(Java 有没有这个)。关于语法:Java是否允许可为空的类型? | 码农家园 (codenong.com)求人不如求己,查了一下还真没有,哈哈哈哈。

这个其实就两种用法:但问号?和双问好??,这两个表示的意义也不一样

单问号:

告诉你,某个不能直接赋值(int a = null,编译不会通过的)的数据类型,可以赋值为空了,戴数据类型是Nullable 的。

一言以蔽之就是:“让不能赋空值的数据类型(如int、double...)可以赋空值”~这么精简,不愧是我,哈哈哈哈

int i; //默认值0
int? ii; //默认值null

双问号:

用于判断一个变量在为 null 的时候返回一个指定的值,用于定义可空类型和引用类型的默认值。

var a=b?? c,将b的值赋给a,如果b为空,则将 c 赋值给a

  static void Main(string[] args)
      {
         
         double? num1 = null;
         double? num2 = 3.14157;
         double num3;
         num3 = num1 ?? 5.34;      // num1 如果为空值则返回 5.34
         Console.WriteLine("num3 的值: {0}", num3);
         num3 = num2 ?? 5.34;
         Console.WriteLine("num3 的值: {0}", num3);
         Console.ReadLine();

      }

最后的结果为:

num1为空,所以给num3赋的值为5.34,而num2不为空,则给num3赋的值为num2的值

num3 的值: 5.34
num3 的值: 3.14157

3、结构体

作为一种值类型(value type),用于组织和存储数据,值类型的数据结构,用来存储各种数据类型的相关数据。轻量级的数据类型!!

定义

struct Books
{
   public string title;
   public string author;
   public string subject;
   public int book_id;
};  

这是结构题的用法:

using System;
using System.Text;
     
struct Books
{
   public string title;
   public string author;
   public string subject;
   public int book_id;
};  

public class testStructure
{
   public static void Main(string[] args)
   {

      Books Book1;        /* 声明 Book1,类型为 Books */
      Books Book2;        /* 声明 Book2,类型为 Books */

      /* book 1 详述 */
      Book1.title = "C Programming";
      Book1.author = "Nuha Ali"; 
      Book1.subject = "C Programming Tutorial";
      Book1.book_id = 6495407;

      /* book 2 详述 */
      Book2.title = "Telecom Billing";
      Book2.author = "Zara Ali";
      Book2.subject =  "Telecom Billing Tutorial";
      Book2.book_id = 6495700;

      /* 打印 Book1 信息 */
      Console.WriteLine( "Book 1 title : {0}", Book1.title);
      Console.WriteLine("Book 1 author : {0}", Book1.author);
      Console.WriteLine("Book 1 subject : {0}", Book1.subject);
      Console.WriteLine("Book 1 book_id :{0}", Book1.book_id);

      /* 打印 Book2 信息 */
      Console.WriteLine("Book 2 title : {0}", Book2.title);
      Console.WriteLine("Book 2 author : {0}", Book2.author);
      Console.WriteLine("Book 2 subject : {0}", Book2.subject);
      Console.WriteLine("Book 2 book_id : {0}", Book2.book_id);       

      Console.ReadKey();

   }
}




//执行结果
Book 1 title : C Programming
Book 1 author : Nuha Ali
Book 1 subject : C Programming Tutorial
Book 1 book_id : 6495407
Book 2 title : Telecom Billing
Book 2 author : Zara Ali
Book 2 subject : Telecom Billing Tutorial
Book 2 book_id : 6495700

这玩意儿怎么看着跟Class 那么像呢???一样的实例化声明额,就是少一个“{set,get}”,但是人家还就是不一样

和类的相同点:

  1. 可带有方法、字段、索引、属性、运算符方法和事件
  2. 可定义构造函数(但是不能定义析构函数,不能有无参构造函数)
  3. 可以实现接口  

和类的不同点:

  1. 不能定义析构函数,不能有无参构造函数(析构函数,用来使用完类之后定义销毁处理的操作,比如释放某些使用的系统资源)
  2. 不能被继承,也不能做基类
  3. 不能指定成员为abstract、virtual、protected
  4. 通常分配在栈上,创建销毁特别快,类则会存储到堆上
  5. 默认可变,也可设置为只读
  6. 结构体是值类型,类是引用类型
  7. 结构体因为是值类型,所以通常不可为空NUll
using System;

// 结构声明
struct MyStruct
{
    public int X;
    public int Y;

    // 结构不能有无参数的构造函数
    // public MyStruct()
    // {
    // }

    // 有参数的构造函数
    public MyStruct(int x, int y)
    {
        X = x;
        Y = y;
    }

    // 结构不能继承
    // struct MyDerivedStruct : MyBaseStruct
    // {
    // }
}

// 类声明
class MyClass
{
    public int X;
    public int Y;

    // 类可以有无参数的构造函数
    public MyClass()
    {
    }

    // 有参数的构造函数
    public MyClass(int x, int y)
    {
        X = x;
        Y = y;
    }

    // 类支持继承
    // class MyDerivedClass : MyBaseClass
    // {
    // }
}

class Program
{
    static void Main()
    {
        // 结构是值类型,分配在栈上
        MyStruct structInstance1 = new MyStruct(1, 2);
        MyStruct structInstance2 = structInstance1; // 复制整个结构

        // 类是引用类型,分配在堆上
        MyClass classInstance1 = new MyClass(3, 4);
        MyClass classInstance2 = classInstance1; // 复制引用,指向同一个对象

        // 修改结构实例不影响其他实例
        structInstance1.X = 5;
        Console.WriteLine($"Struct: {structInstance1.X}, {structInstance2.X}");

        // 修改类实例会影响其他实例
        classInstance1.X = 6;
        Console.WriteLine($"Class: {classInstance1.X}, {classInstance2.X}");
    }
}

2024.4.2就先到这里吧,虽然上班时间比较清闲,但是也不能一致这么闲着啊,再去整一整我的技术分享PPT吧

4、多态和重载

多态性:对继承方法的重新实现

重载:同一个函数名,不同参数的不同实现

抽象方法:“abstract” 一般定义接口使用

必须在抽象类中定义,用“abstract”关键字(不能用在Sealed 的类中)

虚方法:“virtual” 不同继承类中有不同实现 

对虚方法的调用是在运行时发生的,动态多态性是通过抽象类和虚方法实现的

重载后,只会执行新方法的逻辑,不会执行基方法的逻辑,如果想调用基方法的逻辑,需要在方法中使用“base.***()”来实现,下面请看一段代码的演示

using System;
using System.Collections.Generic;

public class Shape
{
    public int X { get; private set; }
    public int Y { get; private set; }
    public int Height { get; set; }
    public int Width { get; set; }
   
    // 虚方法
    public virtual void Draw()
    {
        Console.WriteLine("执行基类的画图任务");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个圆形");
        base.Draw();
    }
}
class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个长方形");
        base.Draw();
    }
}
class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个三角形");
        base.Draw();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 创建一个 List<Shape> 对象,并向该对象添加 Circle、Triangle 和 Rectangle
        var shapes = new List<Shape>
        {
            new Rectangle(),
            new Triangle(),
            new Circle()
        };

        // 使用 foreach 循环对该列表的派生类进行循环访问,并对其中的每个 Shape 对象调用 Draw 方法 
        foreach (var shape in shapes)
        {
            shape.Draw();
        }

        Console.WriteLine("按下任意键退出。");
        Console.ReadKey();
    }

}



画一个长方形
执行基类的画图任务
画一个三角形
执行基类的画图任务
画一个圆形
执行基类的画图任务
按下任意键退出。

5、接口 Interface 

接口定义了属性、方法和事件,这些都是接口的成员、只包含成员的声明,定义则是派生类的任务也,接口提供了派生类应遵循的标准结构结构。定义了实现类继承接口的时候需要实现哪些功能和需要有成员,不同于抽象类,一个类可以实现多个接口,并且抽象类的基类可以有自己的基方法的实现逻辑,抽象类一般用在只有少数方法由基类声明,由派生类实现的情况。

不同于抽象类,基类在使用interface中定义的方法时必须先实现interface中定义的方法,因为interface中并没有该方法的具体实现

using System;

interface IParentInterface
{
    void ParentInterfaceMethod();
}

interface IMyInterface : IParentInterface
{
    void MethodToImplement();
}

class InterfaceImplementer : IMyInterface
{
    static void Main()
    {
        InterfaceImplementer iImp = new InterfaceImplementer();
        iImp.MethodToImplement();
        iImp.ParentInterfaceMethod();
    }

    public void MethodToImplement()
    {
        Console.WriteLine("MethodToImplement() called.");
    }

    public void ParentInterfaceMethod()
    {
        Console.WriteLine("ParentInterfaceMethod() called.");
    }
}


MethodToImplement() called.
ParentInterfaceMethod() called.

6、预处理指令 #***

指导编译器在实际编译开始之前对信息进行预处理

常见的一些预处理指令

预处理器指令描述
#define它用于定义一系列成为符号的字符。
#undef它用于取消定义符号。
#if它用于测试符号是否为真。
#else它用于创建复合条件指令,与 #if 一起使用。
#elif它用于创建复合条件指令。
#endif指定一个条件指令的结束。
#line它可以让您修改编译器的行数以及(可选地)输出错误和警告的文件名。
#error它允许从代码的指定位置生成一个错误。
#warning它允许从代码的指定位置生成一级警告。
#region它可以让您在使用 Visual Studio Code Editor 的大纲特性时,指定一个可展开或折叠的代码块。
#endregion它标识着 #region 块的结束。

空描述有点抽象,还是来个代码吧

#define DEBUG
#define VC_V10
using System;
public class TestClass
{
   public static void Main()
   {

      #if (DEBUG && !VC_V10)
         Console.WriteLine("DEBUG is defined");
      #elif (!DEBUG && VC_V10)
         Console.WriteLine("VC_V10 is defined");
      #elif (DEBUG && VC_V10)
         Console.WriteLine("DEBUG and VC_V10 are defined");
      #else
         Console.WriteLine("DEBUG and VC_V10 are not defined");
      #endif
      Console.ReadKey();
   }
}
//上面的代码在被编译和执行的时候,会输出一下字符串
DEBUG and VC_V10 are defined

7、正则表达式

这个本来不想写来着,因为不同语言用到的基本上是一样的,而且可以当作程序员的一项基本技能了吧,emmm,转念一想,好像不同语言的正则使用方式不一样,那就简单列一个代码吧,具体的匹配规则和使用规则详见这里吧:C# 正则表达式 | 菜鸟教程 (runoob.com),也有在线的正则工具,可以使用,个人感觉还是蛮好用的,菜鸟的就行正则表达式在线测试 | 菜鸟工具 (jyshare.com)

using System;
using System.Text.RegularExpressions;

namespace RegExApplication
{
   class Program
   {
      private static void showMatch(string text, string expr)
      {
         Console.WriteLine("The Expression: " + expr);
         MatchCollection mc = Regex.Matches(text, expr);
         foreach (Match m in mc)
         {
            Console.WriteLine(m);
         }
      }
      static void Main(string[] args)
      {
         string str = "A Thousand Splendid Suns";

         Console.WriteLine("Matching words that start with 'S': ");
         showMatch(str, @"\bS\S*");
         Console.ReadKey();
      }
   }
}






Matching words that start with 'S':
The Expression: \bS\S*
Splendid
Suns
using System;
using System.Text.RegularExpressions;

namespace RegExApplication
{
   class Program
   {
      private static void showMatch(string text, string expr)
      {
         Console.WriteLine("The Expression: " + expr);
         MatchCollection mc = Regex.Matches(text, expr);
         foreach (Match m in mc)
         {
            Console.WriteLine(m);
         }
      }
      static void Main(string[] args)
      {
         string str = "make maze and manage to measure it";

         Console.WriteLine("Matching words start with 'm' and ends with 'e':");
         showMatch(str, @"\bm\S*e\b");
         Console.ReadKey();
      }
   }
}



Matching words start with 'm' and ends with 'e':
The Expression: \bm\S*e\b
make
maze
manage
measure

using System;
using System.Text.RegularExpressions;

namespace RegExApplication
{
   class Program
   {
      static void Main(string[] args)
      {
         string input = "Hello   World   ";
         string pattern = "\\s+";
         string replacement = " ";
         Regex rgx = new Regex(pattern);
         string result = rgx.Replace(input, replacement);

         Console.WriteLine("Original String: {0}", input);
         Console.WriteLine("Replacement String: {0}", result);    
         Console.ReadKey();
      }
   }
}




Original String: Hello   World   
Replacement String: Hello World   

三种代码,应该足够理解和体会正则表达式是如何使用的了

8、异常处理:try、catch、finally、throw

这几个关键字,想必经常敲代码的同学已经再熟悉不过了

  • try:一个 try 块标识了一个将被激活的特定的异常的代码块。后跟一个或多个 catch 块。
  • catch:程序通过异常处理程序捕获异常。catch 关键字表示异常的捕获。
  • finally:finally 块用于执行给定的语句,不管异常是否被抛出都会执行。例如,如果您打开一个文件,不管是否出现异常文件都要被关闭。
  • throw:当问题出现时,程序抛出一个异常。使用 throw 关键字来完成。
try
{
   // 引起异常的语句
}
catch( ExceptionName e1 )
{
   // 错误处理代码
}
catch( ExceptionName e2 )
{
   // 错误处理代码
}
catch( ExceptionName eN )
{
   // 错误处理代码
}
finally
{
   // 要执行的语句
}

常见的集中异常

异常类描述
System.IO.IOException处理 I/O 错误。
System.IndexOutOfRangeException处理当方法指向超出范围的数组索引时生成的错误。
System.ArrayTypeMismatchException处理当数组类型不匹配时生成的错误。
System.NullReferenceException处理当依从一个空对象时生成的错误。
System.DivideByZeroException处理当除以零时生成的错误。
System.InvalidCastException处理在类型转换期间生成的错误。
System.OutOfMemoryException处理空闲内存不足生成的错误。
System.StackOverflowException

处理栈溢出生成的错误。

高级(目前来看,好像是)

特性(Attribute)

跟Java中的 “注释编程” 有点像,这一部分的内容我需要结合B站的一些视频课才能能清楚的理解和掌握,

特性:用于在“运行时”传递程序中各种元素(类、方法、结构、枚举)的行为信息的声明性标签

预定义特性(AttributeUsage、Conditional、Obsolete)

AttributeUsage:描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型

[AttributeUsage(
   validon,    //规定特性可被放置的语言元素,默认值是 AttributeTargets.All
   AllowMultiple=allowmultiple,    //(可选的)是否支持多用 bool值
   Inherited=inherited    //(可选的)是否可被派生 bool值
)]

Conditional:预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符。引起方法调用的条件编译,取决于指定的值,比如 Debug 或 Trace。例如,当调试代码时显示变量的值。

[Conditional("DEBUG")]



#define DEBUG
using System;
using System.Diagnostics;
public class Myclass
{
    [Conditional("DEBUG")]
    public static void Message(string msg)
    {
        Console.WriteLine(msg);
    }
}
class Test
{
    static void function1()
    {
        Myclass.Message("In Function 1.");
        function2();
    }
    static void function2()
    {
        Myclass.Message("In Function 2.");
    }
    public static void Main()
    {
        Myclass.Message("In Main function.");
        function1();
        Console.ReadKey();
    }
}



//在编译和执行时,输出
In Main function.
In Function 1.
In Function 2.

Obsolete: 标记了不应被使用的程序实体,通知编译器丢弃某个特定的目标元素

[Obsolete(   message)]
[Obsolete(   message,   iserror)]


using System;
public class MyClass
{
   [Obsolete("Don't use OldMethod, use NewMethod instead", true)]
   static void OldMethod()
   { 
      Console.WriteLine("It is the old method");
   }
   static void NewMethod()
   { 
      Console.WriteLine("It is the new method"); 
   }
   public static void Main()
   {
      OldMethod();
   }
}


//尝试编译程序的时候
输出: Don't use OldMethod, use NewMethod instead



自定义特性

创建自定义的特性需要四个步骤

  1. 声明自定义特性
  2. 构建自定义特性
  3. 在目标程序元素上应用自定义特性
  4. 通过反射访问特性(包含编写一个简单的程序来读取元数据以便查找各种符号)
// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]

public class DeBugInfo : System.Attribute    //声明了一个名为 DeBugInfo 的自定义特性。
{
  private int bugNo;
  private string developer;
  private string lastReview;
  public string message;    
//所以 bug 编号、开发人员名字和审查日期将是 DeBugInfo 类的必需定位( positional)参数
//消息将是一个可选的命名(named)参数。

  public DeBugInfo(int bg, string dev, string d)
  {
      this.bugNo = bg;
      this.developer = dev;
      this.lastReview = d;
  }//每个特性必须至少有一个构造函数。必需的定位( positional)参数应通过构造函数传递。

  public int BugNo
  {
      get
      {
          return bugNo;
      }
  }
  public string Developer
  {
      get
      {
          return developer;
      }
  }
  public string LastReview
  {
      get
      {
          return lastReview;
      }
  }
  public string Message
  {
      get
      {
          return message;
      }
      set
      {
          message = value;
      }
  }
}



[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]
[DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]
//通过把特性放置在紧接着他的目标之前,来应用该特性
class Rectangle
{
  // 成员变量
  protected double length;
  protected double width;
  public Rectangle(double l, double w)
  {
      length = l;
      width = w;
  }
  [DeBugInfo(55, "Zara Ali", "19/10/2012",
  Message = "Return type mismatch")]
  public double GetArea()
  {
      return length * width;
  }
  [DeBugInfo(56, "Zara Ali", "19/10/2012")]
  public void Display()
  {
      Console.WriteLine("Length: {0}", length);
      Console.WriteLine("Width: {0}", width);
      Console.WriteLine("Area: {0}", GetArea());
  }
}

更详细的解释可以参考这篇文章:C#高级--特性详解_c# 特性-CSDN博客

反射

反射是指:程序可以访问、检测、修改它本身状态或者行为的一种能力

可以使用反射动态的创建类型的实例,讲类型绑定到现有对象,或从现有对象中获取类型,然后可以调用类型的方法或访问其字段和属性。

优点:

  1. 提高程序的灵活性和扩展性
  2. 降低耦合度,提高自适应能力
  3. 允许程序创建和控制任何类的对象,无需提前硬编码目标类

缺点:

  1. 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用
  2. 模糊程序内部逻辑:程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。

用途:

  1. 允许在运行时查看特性”attribute“信息
  2. 允许审查集合中的各种类型,以及实例化这些类型
  3. 允许延迟绑定的方法和属性
  4. 允许在运行时创建新类型,然后是使用这些类型执行一些任务

示例:

我们已经在上面的章节中提到过,使用反射(Reflection)可以查看特性(attribute)信息。

System.Reflection 类的 MemberInfo 对象需要被初始化,用于发现与类相关的特性(attribute)。为了做到这点,您可以定义目标类的一个对象,如下:

using System;

[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute : System.Attribute
{
   public readonly string Url;

   public string Topic  // Topic 是一个命名(named)参数
   {
      get
      {
         return topic;
      }
      set
      {

         topic = value;
      }
   }

   public HelpAttribute(string url)  // url 是一个定位(positional)参数
   {
      this.Url = url;
   }

   private string topic;
}
[HelpAttribute("Information on the class MyClass")]
class MyClass
{
}

namespace AttributeAppl
{
   class Program
   {
      static void Main(string[] args)
      {
         System.Reflection.MemberInfo info = typeof(MyClass);
         object[] attributes = info.GetCustomAttributes(true);
         for (int i = 0; i < attributes.Length; i++)
         {
            System.Console.WriteLine(attributes[i]);
         }
         Console.ReadKey();

      }
   }
}


//当上面的代码被编译和执行时,它会显示附加到类 MyClass 上的自定义特性:
HelpAttribute
using System;
using System.Reflection;
namespace BugFixApplication
{
   // 一个自定义特性 BugFix 被赋给类及其成员
   [AttributeUsage(AttributeTargets.Class |
   AttributeTargets.Constructor |
   AttributeTargets.Field |
   AttributeTargets.Method |
   AttributeTargets.Property,
   AllowMultiple = true)]

   public class DeBugInfo : System.Attribute
   {
      private int bugNo;
      private string developer;
      private string lastReview;
      public string message;

      public DeBugInfo(int bg, string dev, string d)
      {
         this.bugNo = bg;
         this.developer = dev;
         this.lastReview = d;
      }

      public int BugNo
      {
         get
         {
            return bugNo;
         }
      }
      public string Developer
      {
         get
         {
            return developer;
         }
      }
      public string LastReview
      {
         get
         {
            return lastReview;
         }
      }
      public string Message
      {
         get
         {
            return message;
         }
         set
         {
            message = value;
         }
      }
   }
   [DeBugInfo(45, "Zara Ali", "12/8/2012",
        Message = "Return type mismatch")]
   [DeBugInfo(49, "Nuha Ali", "10/10/2012",
        Message = "Unused variable")]
   class Rectangle
   {
      // 成员变量
      protected double length;
      protected double width;
      public Rectangle(double l, double w)
      {
         length = l;
         width = w;
      }
      [DeBugInfo(55, "Zara Ali", "19/10/2012",
           Message = "Return type mismatch")]
      public double GetArea()
      {
         return length * width;
      }
      [DeBugInfo(56, "Zara Ali", "19/10/2012")]
      public void Display()
      {
         Console.WriteLine("Length: {0}", length);
         Console.WriteLine("Width: {0}", width);
         Console.WriteLine("Area: {0}", GetArea());
      }
   }//end class Rectangle  
   
   class ExecuteRectangle
   {
      static void Main(string[] args)
      {
         Rectangle r = new Rectangle(4.5, 7.5);
         r.Display();
         Type type = typeof(Rectangle);
         // 遍历 Rectangle 类的特性
         foreach (Object attributes in type.GetCustomAttributes(false))
         {
            DeBugInfo dbi = (DeBugInfo)attributes;
            if (null != dbi)
            {
               Console.WriteLine("Bug no: {0}", dbi.BugNo);
               Console.WriteLine("Developer: {0}", dbi.Developer);
               Console.WriteLine("Last Reviewed: {0}",
                                        dbi.LastReview);
               Console.WriteLine("Remarks: {0}", dbi.Message);
            }
         }
         
         // 遍历方法特性
         foreach (MethodInfo m in type.GetMethods())
         {
            foreach (Attribute a in m.GetCustomAttributes(true))
            {
               DeBugInfo dbi = (DeBugInfo)a;
               if (null != dbi)
               {
                  Console.WriteLine("Bug no: {0}, for Method: {1}",
                                                dbi.BugNo, m.Name);
                  Console.WriteLine("Developer: {0}", dbi.Developer);
                  Console.WriteLine("Last Reviewed: {0}",
                                                dbi.LastReview);
                  Console.WriteLine("Remarks: {0}", dbi.Message);
               }
            }
         }
         Console.ReadLine();
      }
   }
}




//当上面的代码被编译和执行时,它会产生下列结果:
Length: 4.5
Width: 7.5
Area: 33.75
Bug No: 49
Developer: Nuha Ali
Last Reviewed: 10/10/2012
Remarks: Unused variable
Bug No: 45
Developer: Zara Ali
Last Reviewed: 12/8/2012
Remarks: Return type mismatch
Bug No: 55, for Method: GetArea
Developer: Zara Ali
Last Reviewed: 19/10/2012
Remarks: Return type mismatch
Bug No: 56, for Method: Display
Developer: Zara Ali
Last Reviewed: 19/10/2012
Remarks: 

结束语句:

反射这里理解的不够深刻,用法也理解的不够深刻,这周争取利用空闲时间看看视频

属性

访问器:get、set

索引器

 允许一个对象可以像数组一样使用下标的方式来访问,(有点费解,那就线往下继续看),当为类定义一个索引器时,该类的行为就会像一个虚拟数组一样,这样就像访问数组一样访问类的成员。

用途:声明在某称程度上和属性类似,

//声明格式如下:
element-type this[int index] //“this” 关键字和 “[int index]”
{
   // get 访问器
   get 
   {
      // 返回 index 指定的值
   }

   // set 访问器
   set 
   {
      // 设置 index 指定的值 
   }
}

//下面来看一个实例
using System;
namespace IndexerApplication
{
   class IndexedNames
   {
      private string[] namelist = new string[size];
      static public int size = 10;
      public IndexedNames()
      {
         for (int i = 0; i < size; i++)
         namelist[i] = "N. A.";
      }
      public string this[int index]    //在这里定义了这个类的索引器
      {
         get
         {
            string tmp;

            if( index >= 0 && index <= size-1 )
            {
               tmp = namelist[index];
            }
            else
            {
               tmp = "";
            }

            return ( tmp );
         }
         set
         {
            if( index >= 0 && index <= size-1 )
            {
               namelist[index] = value;
            }
         }
      }

      static void Main(string[] args)
      {
         IndexedNames names = new IndexedNames();    //实例化的时候,自动就会有一个长度为10的List
         names[0] = "Zara";
         names[1] = "Riz";
         names[2] = "Nuha";
         names[3] = "Asif";
         names[4] = "Davinder";
         names[5] = "Sunil";
         names[6] = "Rubic";
         for ( int i = 0; i < IndexedNames.size; i++ )
         {
            Console.WriteLine(names[i]);
         }
         Console.ReadKey();
      }
   }
}


//执行结果为:
Zara
Riz
Nuha
Asif
Davinder
Sunil
Rubic
N. A.
N. A.
N. A.

委托(Delegate)

类似于C或C++中的函数指针,吧某个方法,当作参数一样传递,可以在运行时被改变。

特别用于实现事件和回调方法,所有的委托都派生自System.Delegate类

//声明一个委托的语法
delegate <return type> <delegate-name> <parameter list>
public delegate int MyDelegate (string s);//该委托可被用于引用任何一个带有一个单一的 string 参数的方法,并返回一个 int 类型变量

实例化委托

声明后的委托,要使用“new”关键字来创建,且与一个特定的方法有关。

using System;

delegate int NumberChanger(int n);
namespace DelegateAppl
{
   class TestDelegate
   {
      static int num = 10;
      public static int AddNum(int p)
      {
         num += p;
         return num;
      }

      public static int MultNum(int q)
      {
         num *= q;
         return num;
      }
      public static int getNum()
      {
         return num;
      }

      static void Main(string[] args)
      {
         // 创建委托实例
         NumberChanger nc1 = new NumberChanger(AddNum);
         NumberChanger nc2 = new NumberChanger(MultNum);
         // 使用委托对象调用方法
         nc1(25);
         Console.WriteLine("Value of Num: {0}", getNum());
         nc2(5);
         Console.WriteLine("Value of Num: {0}", getNum());
         Console.ReadKey();
      }
   }
}


//运行结果为为
Value of Num: 35
Value of Num: 175

委托的多播

委托对象可使用 "+" 运算符进行合并,"-" 运算符可用于从合并的委托中移除组件委托。

using System;

delegate int NumberChanger(int n);
namespace DelegateAppl
{
   class TestDelegate
   {
      static int num = 10;
      public static int AddNum(int p)
      {
         num += p;
         return num;
      }

      public static int MultNum(int q)
      {
         num *= q;
         return num;
      }
      public static int getNum()
      {
         return num;
      }

      static void Main(string[] args)
      {
         // 创建委托实例
         NumberChanger nc;
         NumberChanger nc1 = new NumberChanger(AddNum);
         NumberChanger nc2 = new NumberChanger(MultNum);
         nc = nc1;
         nc += nc2;
         // 调用多播
         nc(5);
         Console.WriteLine("Value of Num: {0}", getNum());
         Console.ReadKey();
      }
   }
}


//运行后的结果为:
Value of Num: 75

委托的用途

下面使用两个委托,分别实现,把字符串打印到控制台,和吧字符串打印到文件

using System;
using System.IO;

namespace DelegateAppl
{
   class PrintString
   {
      static FileStream fs;
      static StreamWriter sw;
      // 委托声明
      public delegate void printString(string s);

      // 该方法打印到控制台
      public static void WriteToScreen(string str)
      {
         Console.WriteLine("The String is: {0}", str);
      }
      // 该方法打印到文件
      public static void WriteToFile(string s)
      {
         fs = new FileStream("c:\\message.txt", FileMode.Append, FileAccess.Write);
         sw = new StreamWriter(fs);
         sw.WriteLine(s);
         sw.Flush();
         sw.Close();
         fs.Close();
      }
      // 该方法把委托作为参数,并使用它调用方法
      public static void sendString(printString ps)
      {
         ps("Hello World");
      }
      static void Main(string[] args)
      {
         printString ps1 = new printString(WriteToScreen);
         printString ps2 = new printString(WriteToFile);
         sendString(ps1);
         sendString(ps2);
         Console.ReadKey();
      }
   }
}


//打印结果为
The String is: Hello World

事件

基本上可以认为是一个用户的操作,比如:按键、点击、鼠标移动......或者是一些提示信息,如,系统生成的通知,应用程序需要在事件发生时相应事件,例如中断。

C#中使用事件机制实现线程间的通信

发布器、订阅器

发布器:事件、委托定义在内的对象。事件和委托的联系也定义在这个对象中。

订阅器:接受事件并提供事件处理程序的对象。

事件的声明

public delegate void BoilerLogHandler(string status);

public event BoilerLogHandler BoilerEventLog;    // 基于上面的委托定义事件

//上面的代码定义了一个名为 BoilerLogHandler 的委托和一个名为 BoilerEventLog 的事件,该事件在生成的时候会调用委托。


using System;
namespace SimpleEvent
{
  using System;
  /***********发布器类***********/
  public class EventTest
  {
    private int value;

    public delegate void NumManipulationHandler();


    public event NumManipulationHandler ChangeNum;
    protected virtual void OnNumChanged()
    {
      if ( ChangeNum != null )
      {
        ChangeNum(); /* 事件被触发 */
      }else {
        Console.WriteLine( "event not fire" );
        Console.ReadKey(); /* 回车继续 */
      }
    }


    public EventTest()
    {
      int n = 5;
      SetValue( n );
    }


    public void SetValue( int n )
    {
      if ( value != n )
      {
        value = n;
        OnNumChanged();
      }
    }
  }


  /***********订阅器类***********/

  public class subscribEvent
  {
    public void printf()
    {
      Console.WriteLine( "event fire" );
      Console.ReadKey(); /* 回车继续 */
    }
  }

  /***********触发***********/
  public class MainClass
  {
    public static void Main()
    {
      EventTest e = new EventTest(); /* 实例化对象,第一次没有触发事件 */
      subscribEvent v = new subscribEvent(); /* 实例化对象 */
      e.ChangeNum += new EventTest.NumManipulationHandler( v.printf ); /* 注册   将订阅器的方法传入到事件中 */
      e.SetValue( 7 );    //事件触发
      e.SetValue( 11 );
    }
  }
}



//运行结果为:
event not fire
event fire
event fire

本实例提供一个简单的用于热水锅炉系统故障排除的应用程序。当维修工程师检查锅炉时,锅炉的温度和压力会随着维修工程师的备注自动记录到日志文件中。

using System;
using System.IO;

namespace BoilerEventAppl
{

   // boiler 类
   class Boiler
   {
      private int temp;
      private int pressure;
      public Boiler(int t, int p)
      {
         temp = t;
         pressure = p;
      }

      public int getTemp()
      {
         return temp;
      }
      public int getPressure()
      {
         return pressure;
      }
   }
   // 事件发布器
   class DelegateBoilerEvent
   {
      public delegate void BoilerLogHandler(string status);

      // 基于上面的委托定义事件
      public event BoilerLogHandler BoilerEventLog;

      public void LogProcess()
      {
         string remarks = "O. K";
         Boiler b = new Boiler(100, 12);
         int t = b.getTemp();
         int p = b.getPressure();
         if(t > 150 || t < 80 || p < 12 || p > 15)
         {
            remarks = "Need Maintenance";
         }
         OnBoilerEventLog("Logging Info:\n");
         OnBoilerEventLog("Temparature " + t + "\nPressure: " + p);
         OnBoilerEventLog("\nMessage: " + remarks);
      }

      protected void OnBoilerEventLog(string message)
      {
         if (BoilerEventLog != null)
         {
            BoilerEventLog(message);
         }
      }
   }
   // 该类保留写入日志文件的条款
   class BoilerInfoLogger
   {
      FileStream fs;
      StreamWriter sw;
      public BoilerInfoLogger(string filename)
      {
         fs = new FileStream(filename, FileMode.Append, FileAccess.Write);
         sw = new StreamWriter(fs);
      }
      public void Logger(string info)
      {
         sw.WriteLine(info);
      }
      public void Close()
      {
         sw.Close();
         fs.Close();
      }
   }
   // 事件订阅器
   public class RecordBoilerInfo
   {
      static void Logger(string info)
      {
         Console.WriteLine(info);
      }//end of Logger

      static void Main(string[] args)
      {
         BoilerInfoLogger filelog = new BoilerInfoLogger("e:\\boiler.txt");
         DelegateBoilerEvent boilerEvent = new DelegateBoilerEvent();
         boilerEvent.BoilerEventLog += new 
         DelegateBoilerEvent.BoilerLogHandler(Logger);
         boilerEvent.BoilerEventLog += new 
         DelegateBoilerEvent.BoilerLogHandler(filelog.Logger);
         boilerEvent.LogProcess();
         Console.ReadLine();
         filelog.Close();
      }//end of main

   }//end of RecordBoilerInfo
}


//当上面的代码被编译和执行时,会产生如下结果
Logging info:

Temperature 100
Pressure 12

Message: O. K

集合(Collection)

专门用于数据存储和检索的类。这些类提供了对栈(stack)、队列(queue)、列表(list)和哈希表(hash table)的支持。大多数集合类实现了相同的接口

常见的集合类以及用法

  1. 动态数组(ArrayList):
  2. 哈希表(Hashtable):
  3. 排序列表(SortedList):
  4. 堆栈(Stack):
  5. 队列(Queue):
  6. 点阵列(BitArray):
  7. 列表(List):

这里感觉自己并没找到他们具体的区别,具体怎么使用以及技术细节,还需要进一步的学习。暂时先搁置吧C# 集合(Collection) | 菜鸟教程 (runoob.com)

泛型

允许延迟编写类或方法中的编程元素的数据类型的规范,直到在程序中使用它的时候。

使用泛型是一种增强程序功能的技术,具体特性表现为

  1. 可以最大限度的重用代码、保护类型的安全、提高性能
  2. 可以创建泛型集合类,可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托
  3. 可以对泛型类进行约束以访问特定数据类型的方法
  4. 关于泛型数据类型中使用的类型的信息可以在运行时通过使用反射获取

泛型方法

using System;
using System.Collections.Generic;

namespace GenericMethodAppl
{
    class Program
    {
        static void Swap<T>(ref T lhs, ref T rhs)
        {
            T temp;
            temp = lhs;
            lhs = rhs;
            rhs = temp;
        }
        static void Main(string[] args)
        {
            int a, b;
            char c, d;
            a = 10;
            b = 20;
            c = 'I';
            d = 'V';

            // 在交换之前显示值
            Console.WriteLine("Int values before calling swap:");
            Console.WriteLine("a = {0}, b = {1}", a, b);
            Console.WriteLine("Char values before calling swap:");
            Console.WriteLine("c = {0}, d = {1}", c, d);

            // 调用 swap
            Swap<int>(ref a, ref b);
            Swap<char>(ref c, ref d);

            // 在交换之后显示值
            Console.WriteLine("Int values after calling swap:");
            Console.WriteLine("a = {0}, b = {1}", a, b);
            Console.WriteLine("Char values after calling swap:");
            Console.WriteLine("c = {0}, d = {1}", c, d);
            Console.ReadKey();
        }
    }
}


//执行之后会产生如下效果
/*
Int values before calling swap:
a = 10, b = 20
Char values before calling swap:
c = I, d = V
Int values after calling swap:
a = 20, b = 10
Char values after calling swap:
c = V, d = I
*/

泛型委托

delegate T NumberChanger<T>(T n);    //泛型委托的声明



using System;
using System.Collections.Generic;

delegate T NumberChanger<T>(T n);
namespace GenericDelegateAppl
{
    class TestDelegate
    {
        static int num = 10;
        public static int AddNum(int p)
        {
            num += p;
            return num;
        }

        public static int MultNum(int q)
        {
            num *= q;
            return num;
        }
        public static int getNum()
        {
            return num;
        }

        static void Main(string[] args)
        {
            // 创建委托实例
            NumberChanger<int> nc1 = new NumberChanger<int>(AddNum);
            NumberChanger<int> nc2 = new NumberChanger<int>(MultNum);
            // 使用委托对象调用方法
            nc1(25);
            Console.WriteLine("Value of Num: {0}", getNum());
            nc2(5);
            Console.WriteLine("Value of Num: {0}", getNum());
            Console.ReadKey();
        }
    }
}

//运行结果为:
Value of Num: 35
Value of Num: 175
using System;
using System.Web.Caching;

namespace Xy.CacheManager
{
  public class CacheHelper<T>
  {
      //获取缓存实体
      public static T Get(Cache cache,string cacheKey)
      {
        //....缓存操作
      } 
      //插入缓存
      public static void Set(Cache cache T tEntity,string cacheKey)
      {
        //....缓存操作
      }
  }
}

泛型约束

在声明泛型方法/泛型类的时候,可以给泛型加上一定的约束来满足我们特定的一些条件。

using System;
using System.Web.Caching;

namespace Demo.CacheManager
{
   public class CacheHelper<T> where T:new()
   {
      
   }
}

匿名方法

委托:将一个方法当成参数,传递

匿名方法:传递代码块作为委托参数的技术,没有名称只有主体的方法

匿名方法不需要指定返回类型,它会从方法主体内的 reture 语句推断

语法

delegate void NumberChanger(int n);
...
NumberChanger nc = delegate(int x)
{
    Console.WriteLine("Anonymous Method: {0}", x);
};

/*
代码块 Console.WriteLine("Anonymous Method: {0}", x); 是匿名方法的主体。

委托可以通过匿名方法调用,也可以通过命名方法调用,即,通过向委托对象传递方法参数。

注意: 匿名方法的主体后面需要一个 ;。
*/

//例如
nc(10);

可以通过这个实例来体会匿名方法

using System;

delegate void NumberChanger(int n);
namespace DelegateAppl
{
    class TestDelegate
    {
        static int num = 10;
        public static void AddNum(int p)
        {
            num += p;
            Console.WriteLine("Named Method: {0}", num);
        }

        public static void MultNum(int q)
        {
            num *= q;
            Console.WriteLine("Named Method: {0}", num);
        }

        static void Main(string[] args)
        {
            // 使用匿名方法创建委托实例
            NumberChanger nc = delegate(int x)
            {
               Console.WriteLine("Anonymous Method: {0}", x);
            };
            
            // 使用匿名方法调用委托
            nc(10);

            // 使用命名方法实例化委托
            nc =  new NumberChanger(AddNum);
            
            // 使用命名方法调用委托
            nc(5);

            // 使用另一个命名方法实例化委托
            nc =  new NumberChanger(MultNum);
            
            // 使用命名方法调用委托
            nc(2);
            Console.ReadKey();
        }
    }
}


/*
运行结果为

Anonymous Method: 10
Named Method: 15
Named Method: 30

*/

C#2.0中引入了Lambda表达式

/*
Lambda 表达式是一种更加简洁的语法形式,用来编写匿名方法
*/

using System;

delegate void NumberChanger(int n);

namespace DelegateAppl
{
    class TestDelegate
    {
        static int num = 10;

        public static void AddNum(int p)
        {
            num += p;
            Console.WriteLine("Named Method: {0}", num);
        }

        public static void MultNum(int q)
        {
            num *= q;
            Console.WriteLine("Named Method: {0}", num);
        }

        static void Main(string[] args)
        {
            // 使用 lambda 表达式创建委托实例
            NumberChanger nc = x => Console.WriteLine($"Lambda Expression: {x}");

            // 使用 lambda 表达式调用委托
            nc(10);

            // 使用命名方法实例化委托
            nc = new NumberChanger(AddNum);

            // 使用命名方法调用委托
            nc(5);

            // 使用另一个命名方法实例化委托
            nc = new NumberChanger(MultNum);

            // 使用命名方法调用委托
            nc(2);

            Console.ReadKey();
        }
    }
}

不安全代码

当一个代码块使用unsafe修饰符标记时,C#允许在函数中使用指针变量

指针

指针是值为另一个变量的地址变量,即内存位置的直接地址

实例描述
int* pp 是指向整数的指针。
double* pp 是指向双精度数的指针。
float* pp 是指向浮点数的指针。
int** pp 是指向整数的指针的指针。
int*[] pp 是指向整数的指针的一维数组。
char* pp 是指向字符的指针。
void* pp 是指向未知类型的指针。
int* p1, p2, p3;     // 正确  
int *p1, *p2, *p3;   // 错误 


//下面是一个指针的实例
using System;
namespace UnsafeCodeApplication
{
    class Program
    {
        static unsafe void Main(string[] args)
        {
            int var = 20;
            int* p = &var;
            Console.WriteLine("Data is: {0} ",  var);
            Console.WriteLine("Address is: {0}",  (int)p);
            Console.ReadKey();
        }
    }
}


//执行后的结果为
Data is: 20
Address is: 99215364

也可以只声明某一段代码为不安全代码,只需要声明方法的一部分为不安全代码

using System;
namespace UnsafeCodeApplication
{
   class Program
   {
      public static void Main()
      {
         unsafe  //此处声明下面的代码块为 不安全代码
         {
            int var = 20;
            int* p = &var;
            Console.WriteLine("Data is: {0} " , var);
            Console.WriteLine("Data is: {0} " , p->ToString());
            Console.WriteLine("Address is: {0} " , (int)p);
         }
         Console.ReadKey();
      }
   }
}


Before Swap: var1: 10, var2: 20
After Swap: var1: 20, var2: 10

使用指针访问数组元素

数组名称  ≠  具有相同数据类型的指针   int *p   ≠   int[] p

int *p 在内存中不是固定的。如果您需要使用指针变量访问数组数据,可以像我们通常在 C 或 C++ 中所做的那样,使用 fixed 关键字来固定指针。

using System;
namespace UnsafeCodeApplication
{
   class TestPointer
   {
      public unsafe static void Main()
      {
         int[]  list = {10, 100, 200};
         fixed(int *ptr = list)

         /* 显示指针中数组地址 */
         for ( int i = 0; i < 3; i++)
         {
            Console.WriteLine("Address of list[{0}]={1}",i,(int)(ptr + i));
            Console.WriteLine("Value of list[{0}]={1}", i, *(ptr + i));
         }
         Console.ReadKey();
      }
   }
}



Address of list[0] = 31627168
Value of list[0] = 10
Address of list[1] = 31627172
Value of list[1] = 100
Address of list[2] = 31627176
Value of list[2] = 200

编译不安全代码

为了编译不安全代码,您必须切换到命令行编译器指定   /unsafe  命令行

例如,为了编译包含不安全代码的名为 prog1.cs 的程序,需在命令行中输入命令:

csc /unsafe prog1.cs

如果您使用的是 Visual Studio IDE,那么您需要在项目属性中启用不安全代码。

步骤如下:

  • 通过双击资源管理器(Solution Explorer)中的属性(properties)节点,打开项目属性(project properties)
  • 点击 Build 标签页。
  • 选择选项"Allow unsafe code"。

多线程

线程是轻量级进程

之前自己做过一点多线程相关的学习,但是不深刻,基于bilibili  AV:BV16G4y1c72R

  • 单线程做菜:

    无法拖动窗口,影响ui界面

    Thread做菜:

    重新开了一个线程====》实例化一个Thread t,然后t.start() 新开一个线程运行。要用到lambda表达式

    Task做菜

    同上   但是优于Thread,因为有进程管理等相关功能Task.Run(()=>{Thread.sleep(3000);MessageBox.Show("素菜做好了");Thread.Sleep(5000);MessageBox.Show("荤菜做好了");})

    同时做多道菜

    两个锅做菜

    Task.Run(()=>{Thread.sleep(3000);MessageBox.Show("素菜做好了");});

    Task.Run(()=>{Thread.Sleep(5000);MessageBox.Show("荤菜做好了");})

    菜都做好了再吃饭

    等到Task.Run执行完之后才会继续向下执行,否则“才还没做好,但是已经喊人吃饭了”

    多个锅同时做菜,怎们办?用Task列表

线程的生命周期

开始于 System.Threading.Thread 类的对象被创建时,结束于线程被终止或完成执行时,下面是线程生命周期的各种状态

  • 未启动状态:当线程实例被创建但 Start 方法未被调用时的状况。
  • 就绪状态:当线程准备好运行并等待 CPU 周期时的状况。
  • 不可运行状态:下面的几种情况下线程是不可运行的:

    • 已经调用 Sleep 方法
    • 已经调用 Wait 方法
    • 通过 I/O 操作阻塞
  • 死亡状态:当线程已完成执行或已中止时的状况。

主线程

在 C# 中,System.Threading.Thread 类用于线程的工作。它允许创建并访问多线程应用程序中的单个线程。进程中第一个被执行的线程称为主线程。主线程自动创建。可以使用 Thread 类的 CurrentThread 属性访问线程。

线程的属性和方法详见C# 多线程 | 菜鸟教程 (runoob.com)或者微软文档

创建线程的代码演示如下

using System;
using System.Threading;

namespace MultithreadingApplication
{
    class ThreadCreationProgram
    {
        public static void CallToChildThread()    //子线程要调用的方法   一个静态方法
        {
            Console.WriteLine("Child thread starts");
        }
        
        static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);     //初始化一个子线程
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(childref);
            childThread.Start();
            Console.ReadKey();
        }
    }
}


In Main: Creating the Child thread
Child thread starts

线程管理

Thread.sleep()、Thread.Abort()销毁线程

结束语

OK 经过一周多一点的时间,终于把C#里面语法,较为高级的地方,粗略的过了一遍,后续有自己新理解的地方,应该也会重新加入

本来上周就应该结束的内容,结果拖到现在,清明假期三天啥都没干,结果,今天周二(清明调休)正好也是i三天,哎,以后假期不能偷懒了,这种被迫赶任务的感觉其实有带你不太爽

这一周接下来的时间就都用来每天一套C#自测题,并且每天复习一遍自己写的这些内容吧,加油!!!啊,对这周还要把Winform的内容看一部份,每天2个视频,不过分吧,我感觉不过分,看完视频也得写总结,而且还要多练,不然又要忘掉。加油!!!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值