C#学习笔记——需要注意的基础知识

Posted 桫椤

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#学习笔记——需要注意的基础知识相关的知识,希望对你有一定的参考价值。

  • #region 和#endregion 关键字可以对代码分为几个片段进行说明注释,且可以展开和折叠该段代码区域。
  • 基本类型的别名及取值范围:

    以上数字后面得加字母的,如float值后面必须加上F,不然会被编译器当作double处理。
  • 4.2f == 4.2d 结果为false
  • const:编译时确定的值,不可能在运行时改变;常量字段自动成为静态字段,但不能显示声明为static;只能使用包含字面值的类型(如string int double等)。
    如果一个程序集引用了另一个程序集中的常量,常量值将直接编译到引用程序集中。所以,如果引用程序集中的值发生改变,而且引用程序集没有重新编译,那么引用程序集将继续使用原始值,而不是新值。将来可能改变的值应该指定为readonly,不要指定为常量。
  • readonly:只能用于字段(不能用于局部变量),表示这个字段只能在执行构造函数的过程中赋值,或由初始化赋值语句赋值;不同于const字段,每个实例的readonly字段都可以不同,可以在构造器中更改;即可以是实例字段也可以是静态字段;readonly字段不限于包含字面值的类型,可以是自定义的class;将readonly应用于数组,不会冻结数组的内容,只会冻结数组中的元素数量,因为无法将只读字段重新赋给新的实例,数组中的元素仍然是可写的。
  • 使用Unicode转义序列可以指定Unicode 字符,该转义序列包括标准的\\字符,后跟一个u和一个4位十六进制值(例如,单引号的unicode是0x0027)。下面的字符串是等价的:
    "Karli\\\'s string."
    "Karli\\u0027s string."

  • 字符串之前加一个@字符, 即两个双引号之间的所有字符都包含在字符串中,包括行末字符和需要转义的字符。唯一例外是双引号字符的转义,它们必须指定,以免结束字符串。

  • using 语句还可以为名称空间提供一个别名。

    1 namespace LevelOne
    2 {
    3     using LT = LevelTwo;
    4     // name "NameThree" defined
    5     namespace LevelTwo
    6     {
    7         // name "NameThree" defined
    8     }
    9 }

    LevelOne名称空间中的代码可以把 LevelOne.NameThree 引用为NameThree , 把LevelOne.LevelTwo.NameThree 引用为LT.NameThree。

  • 在C++中,可以在运行完一个 case 语句后,运行另一个 case 语句。C#不行,每一个分支中的代码最后必须要有一个break,除非多个case 语句放在一起:

    switch(<testVar>)
    {
        case <comparisonVal1>:
        case <comparisonVal2>:
            <code to execute>
            break;
        ...
    }
  • checked 和unchecked,称为表达式的溢出检查上下文。
  • 把 string 转换为枚举植:(enumerationType)Enum.Parse(typeof(enumerationType), enumerationValueString);
    string myString = "north";
    Orientation myDirection = (Orientation)Enum.Pase(typeof(Orientation), myString);
  • 枚举值作为标志使用:


     

  • 如果使用变量定义数组大小,该变量必须是一个常量。
  • foreach 循环对数组内容进行只读访问,所以不能改变任何元素的值。例如,不能编写如下代码
    foreach(string name in friendnames)
    {
        name = "Rupert the bear";
    }
  • 多维数组:如二维,<baseType>[,] <name>; 访问hillHeight[2,1]
  • 数组的数组(锯齿数组或者不定数组):如

    int[][] jaggedIntArray;

    初始化:

    int[][] jaggedIntArray;
    jaggedIntArray = new int[2][];
    jaggedIntArray[0] = new int[3];
    jaggedIntArray[1] = new int[4];

    或:

    jaggedIntArray = new int[3][]{new int[]{1,2,3},
                                new int[]{1},
                                new int[]{1,2}};

    或:

    int[][] jaggedIntArray = {new int[]{1,2,3},
                                new int[]{1},
                                new int[]{1,2}};

    对数组的数组(锯齿数组或者不定数组)可以使用foreach 循环,但如下使用会出错(普通二维等多维数组可以):有数组divisors1To10是不定数组:

    foreach(int divisor in divisors1To10)
    {
        Console.WriteLine(divisor);
    }

    因为数组divisors1To10 包含int[]元素,而不是int 元素。必须循环每个子数组和数组本身:

    foreach(int[] divisorofInt in divisors1To10)
    {
        foreach(int divisor in divisorofInt)
        {
            Console.WriteLine(divisor);        
        }
    }
  • C#允许为函数指定一个(只能指定一个)特定的参数,这个参数必须是函数定义中的最后一个参数,称为参数数组。参数数组可以使用个数不定的参数调用函数,可以使用params 关键字定义它们。如:
  • 函数传引用:ref,定义和使用时都得加ref:

    static void ShowDouble(ref int val);

    ShowDouble(ref myNumber);

  •  

    输出参数:out,使用方式与 ref关键字相同(在函数定义和函数调用中用作参数的修饰符)。但注意:

    1、把未赋值的变量用作ref参数是非法的,但可以把未赋值的变量用作out参数。

    2、另外,在函数使用out参数时,out参数必须看作是还未赋值。

    即调用代码可以把已赋值的变量用作out参数,存储在该变量中的值会在函数执行时丢失。
  • 委托:同C中的函数指针,在定义该类型时在函数原型前加delegate即可
  • 输出调试信息: 在运行期间把文本写入“输出”(Output) 窗口。

    1.Debug.WriteLine(),仅在调试模式下运行, 在发布版本中,该命令会消失

    2.Trace.WriteLine() , 可用于发布程序

    类似还有:

    Debug.Write()

    Trace.Write()

    Debug.WriteLineIf()

    Trace.WriteLineIf()

    Debug.WriteIf()

    Trace.WriteIf()

  • 判定语句(assertion)时中断:

    Debug.Assert()

    Trace.Assert()

  • try ... catch ... finally:只有 try 块和 finally 块,而没有catch块,或者有一个try 块和好几个 catch块。如果有一个或多个 catch块,finally 块就是可选的,否则就是必需的。
  • 静态构造函数:一个类只能有一个静态构造函数,该构造函数不能有访问修饰符,也不能带任何参数。静态构造函数不能直接调用,只能在下述情况下执行:

      创建包含静态构造函数的类实例时

      访问包含静态构造函数的类的静态成员时

    无论创建了多少个类实例,其静态构造函数都只调用一次。
  • 静态类:类只包含静态成员,且不能用于实例化对象(如Console)。
  • 接口是把公共实例(非静态)方法和属性组合起来,以封装特定功能的一个集合。接口不能单独存在。不能像实例化一个类那样实例化接口。另外,接口不能包含实现其成员的任何代码,而只能定义成员本身。实现过程必须在实现接口的类中完成。接口的名称一般用大写字母I 开头(如IHotDrink)

  • 可删除的对象:支持IDisposable 接口的对象必须实现其Dispose()方法。C#允许使用一种可以优化使用这个方法的结构。using 关键字可以在代码块中初始化使用重要资源的对象,会在这个代码块的末尾自动调用Dispose()方法,用法如下:
    或者把初始化时象<VariableName>作为 using语句的一部分:

    在这两种情况下,可以在using代码块中使用变量<VariableName>,并在代码块的末尾自动删除(在代码块执行完毕后,调用Dispose())。

  • 定义类时可加的修饰符:

    internal:类声明为内部的,即只有当前项目中的代码才能访问它。不加则默认为该值。

    public:类是公共的,应该可以由其他项目中的代码来访问。

    不可以是protect或private,还有两个可选的互斥的修饰符:

    abstract 或 sealed:抽象的(不能实例化,只能继承,可以有抽象成员)或密封的(sealed,不能继承)。

    下表是类定义中可以使用的访问修饰符的组合:

  • 如果指定了基类,它必须紧跟在冒号的后面,之后才是指定的接口。如果没有指定基类,则接口就跟在冒号的后面。必须使用逗号分隔基类名(如果有基类)和接口名。
  • 接口定义跟类基本一样,只是不能在接口中使用关键字 abstract 和 sealed

  • 联合使用 GetType()和typeof(这是一个C#运算符,可以把类名转换为System.Type 对象),就可以进行比较,如下所示:

  • 无论在派生类上使用什么构造函数(默认的构造函数或非默认的构造函数),除非明确指定,否则就使用基类的默认构造函数。可以在派生类的构造函数定义中指定所使用的基类构造函数,base 关键字指定.NET 实例化过程使用基类中有指定参数的构造函数。如下示例就会调用基类中带一个参数的构造函数:(如果没有给构造函数指定构造函数初始化器,编译器就会自动添加 base()调用默认构造函数)




    除了base 关键字外,这里还可以将另一个关键字this 用作构造函数初始化器。这个关键字指定在调用指定的构造函数前,.NET 实例化过程对当前类使用非默认的构造函数。例如:(通常使用构造函数初始化器只能指定一个构造函数。)

    这段代码将执行下述序列:

      执行System.Object.Object 构造函数。
      执行MyBaseClass.MyBaseClass(int i)构造函数。
      执行MyDerivedClass.MyDerivedClass(int i, int j)构造函数。
      执行MyDerivedClass.MyDerivedClass()构造函数。

  • 结构是值类型,而类是引用类型,传递类变量时实际传的是指针,对其一的变量操作也会改变另一变量的值。

  • 访问器,分别用get 和set 关键字来定义,可以用于控制对属性的访问级别。可以忽略其中的一个块来创建只读或只写属性(忽略 get 块创建只写属性,忽略 set 块创建只读属性)。如下定义一个属性:

  • 简单的属性一般与私有字段相关联,以控制对这个字段的访问,使用关键字value 表示用户提供的属性值:
  • 重写或隐藏的基类方法: 如果继承的成员是虚拟的,就可以用override 关键字重写这段实现代码。无论继承的成员是否为虚拟,都可以用new关键字(非必须,不加会有警告)隐藏这些实现代码。

    其中重写方法将替换基类中的实现代码,即使这是通过基类类型进行的,也就将使用新版本:

    对于基类的虚拟方法和非虚拟方法来说,尽管隐藏了基类的实现代码,但仍可以通过基类访问它:只更改上面例子的一行,结果变为Base imp
  • 接口成员的定义与类成员的定义相似,但有几个重要的区别:

     不允许使用访问修饰符(public、private、protected 或internal),所有的接口成员都是公共的。

     接口成员不能包含代码体。

     接口不能定义字段成员。

     接口成员不能用关键字static、virtual、abstract 或sealed 来定义。

     类型定义成员是禁止的。

    但要隐藏继承了基接口的成员,可以用关键字new 来定义它们,例如:

  • 实现接口的类必须包含该接口所有成员的实现代码,且必须匹配指定的签名(包括匹配指定的get 和set 块),并且必须是public的, 可以使用关键字 virtual 或 abstract 来实现接口成员,但不能使用 static 或 const。还可以在基类上实现接口成员,例如:

    继承一个实现给定接口的基类,就意味着派生类隐式地支持这个接口。如果在基类中把实现代码定义为虚拟,派生类就可以替换该实现代码,而不是隐藏它们。如果要使用new 关键字隐藏一个基类成员,而不是重写它,则方法IMyInterface.DoSomething()就总是引用基类版本,即使通过这个接口来访问派生类,也是这样。


    以上示例是隐式地实现接口成员,那么该成员可以通过类和接口来访问:

    或者:

    也可以由类显式地实现接口成员。如果这么做,该成员就只能通过接口来访问,不能通过类来访问:

    其中 DoSomething()是显式实现的,而 DoSomethingElse()是隐式实现的。只有后者可以直接通过MyClass 的对象实例来访问。

    前面说过,如果实现带属性的接口,就必须实现匹配的get/set存取器。这并不是绝对正确的——如果在定义属性的接口中只包含set 块,就可给类中的属性添加get 块,反之亦然。但是,只有所添加的存取器的可访问修饰符比接口中定义的存取器的可访问修饰符更严格时,才能这么做。因为按照定义,接口定义的存取器是公共的,也就是说,只能添加非公共的存取器。例如:
  • 部分类定义:如果所创建的类包含一种类型或其他类型的许多成员时,就很容易混淆,代码文件也比较长时可以使用部分类定义,把类的定义放在多个文件中。例如,可以把字段、属

    性和构造函数放在一个文件中,而把方法放在另一个文件中。为此,只需在每个包含部分类定义的文件中对类使用partial 关键字即可,如下所示:

    如果使用部分类定义,partial 关键字就必须出现在包含定义部分的每个文件的与此相同的位置。应用于部分类的接口也会应用于整个类,也就是说,下面的两个定义是等价的:


    部分类定义可以在一个部分类定义文件或者多个部分类定义文件中包含基类。但如果基类在多个定义文件中指定,它就必须是同一个基类,因为在C#中,类只能继承一个基类。
  • 部分方法定义,部分方法在部分类中定义,但没有方法体,在另一个部分类中包含实现代码。部分方法也可以是静态的,但它们总是私有的,且不能有返回值。它们使用的任何参数都不能是out 参数,但可以是ref 参数。部分方法也不能使用virtual、abstract、override、new、sealed 和extern修饰符。有了这些限制,就不太容易看出部分方法的作用了。实际上,部分方法在编译代码时非常重要,其用法倒并不重要。考虑下面的代码:

    在控制台应用程序中调用DoSomething 时,输出如下内容:

    如果删除第二个部分类定义,或者删除部分方法的全部执行代码(注释掉代码),输出就如下

    所示:

    编译代码时,如果代码包含一个没有实现代码的部份方法,编译器会完全删除该方法,还会删除对该方法的所有调用。执行代码时,不会检查实现代码,因为没有检查方法的调用。这会略微提高性能。与部分类一样,在定制自动生成的代码或设计器创建的代码时,部分方法是很有用的。设计器会声明部分方法,用户根据具体情形选择是否实现它。如果不实现它,就不会影响性能,因为该方法在编译过的代码中不存在。所以部分方法不能有返回类型。

  • 封箱(boxing)是把值类型转换为System.Object 类型,或者转换为由值类型实现的接口类型。

    拆箱(unboxing)是相反的转换过程。
    原理:

    故注意,无论是装箱还是拆箱都是对值进行复制,所以任何对拆装箱后的值进行修改都不会影响到原值!详见最下面的例子。

    也可以把值类型封箱到一个接口类型中,只要它们实现这个接口即可:

  • 下面例子将更好理解值类型不可变的重要性:

     

     

  • is 运算符并不是说明对象是某种类型的一种方式,而是可以检查对象是否是给定类型,或者是否可以转换为给定类型,如果是,这个运算符就返回 true :<operand> is <type>

    如果<type >是一个类类型,而<operand>也是该类型,或者它继承了该类型,或者它可以封箱到该类型中,则结果为true 。

    如果<type >是一个接口类型,而<operand>也是该类型,或者它是实现该接口的类型,则结果为 true 。

    如果<type >是一个值类型,而<operand>也是该类型,或者它可以拆箱到该类型中,则结果为true 。

  • as 运算符使用下面的语法,把一种类型转换为指定的引用类型:  <operand> as <type>

    这只适用于下列情况:

      <operand>的类型是<type >类型

      <operand>可以隐式转换为<type >类型

      <operand>可以封箱到<type >类型中

    如果不能从<operand>转换为<type >,则表达式的结果就是null

  • 定义可空类型:泛型使用 System.Nullable<T> 类型提供了使值类型为空的一种方式。例如:

             System.Nullable<int> nullableInt;

    int? 是System.Nullable<int>的缩写,但更便于读取:

             int? nullableInt;

    如果 op1 是null ,就会生成System.InvalidOperationException 类型的异常。

    ??运算符:空接合运算符(null coalescing operator) ,是一个二元运算符,允许给可能等于null

    的表达式提供另一个值。如果第一个操作数不是null,该运算符就等于第一个操作数,否则,该运算符就等于第二个操作数。下面的两个表达式的作用是相同的:

  • 对象初始化器(object initializer) ,这是无需在类中添加额外的代码(如此处详细说明的构造函数)就可以实例化和初始化对象的方式:

    如:

  • 可以实现一个迭代器,来控制循环代码如何在其循环过程中在取值。要迭代一个类,需要实现GetEnumerator()方法,其返回类型是IEnumerator 。要迭代类的成员,例如方法,可以使用IEnumerable返回类型。在迭代器的代码块中,使用yield 关键字返回值。如:
  • Lambda表达式:可用于创建委托或表达式目录树类型的匿名函数。通过使用 lambda 表达式,可以写入可作为参数传递或作为函数调用值返回的本地函数。Lambda 表达式对于编写 LINQ 查询表达式特别有用。

    若要创建 Lambda 表达式,需要在 Lambda 运算符 => 左侧指定输入参数(如果有),然后在另一侧输入表达式或语句块。例如,lambda 表达式 x => x * x 指定名为 x 的参数并返回 x 的平方值。如下面的示例所示,你可以将此表达式分配给委托类型:

    delegate int del(int i);
    static void Main(string[] args)
    {
        del myDelegate = x => x * x;
        int j = myDelegate(5); //j = 25
    }

    仅当 lambda 只有一个输入参数时,括号才是可选的;否则括号是必需的。括号内的两个或更多输入参数使用逗号加以分隔;有时,编译器难以或无法推断输入类型。如果出现这种情况,你可以按以下示例中所示方式显式指定类型;使用空括号指定零个输入参数:

    (x, y) => x == y
    
    (int x, string s) => s.Length > x
    
    () => SomeMethod()

    此处显示了一个标准查询运算符,Count<TSource> 方法,编译器可以推断输入参数的类型,或者你也可以显式指定该类型。这个特殊 lambda 表达式将计算那些除以 2 时余数为 1 的整数的数量 (n):

    int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
    int oddNumbers = numbers.Count(n => n % 2 == 1);

以上是关于C#学习笔记——需要注意的基础知识的主要内容,如果未能解决你的问题,请参考以下文章

(原创)C#零基础学习笔记009-异常处理

C# 学习笔记(18)操作SQL Server 中

C#学习笔记(基础知识回顾)之枚举

WPF学习笔记之如何通过后台C#代码添加(增/删/改按钮)实现对SQLServer数据库数据的更改

C#基础及CLR基础学习笔记

零基础学python需要注意哪些问题