C# 7.0简而言之 -- 02. C#基础 (1)

时间:2022-05-12
本文章向大家介绍C# 7.0简而言之 -- 02. C#基础 (1),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

第一个C#程序

using System; // 引入命名空间
class Test // 声明类
{
    static void Main() // 方法声明
    {
        int x = 12 * 30; // 语句1
        Console.WriteLine(x); // 语句2
    } // 方法结束
} // 类结束

程序的核心是这两个语句:

        int x = 12 * 30; // 语句1
        Console.WriteLine(x); // 语句2

C#中的语句(Statement)是顺序执行的, 以分号结束(或者程序块).

语句1里面计算了表达式(expression) 12 * 30, 并把结果保存到了本地变量x里面, x是整型类型.

语句2调用了Console类的WriteLine方法, 把变量x的打印到了文字窗口.

方法通过执行一系列语句(statement)来完成某个动作, 这一系列的语句叫做语句块(statement block)----就是指大括号之间包含了0个或多个语句的部分.

方法可以从调用者那里通过指定参数来接收输入的数据, 然后通过返回类型把输出数据返回给调用者.

static int FeetToInches(int feet) {...}

这里面, 第一个int是指方法的返回类型, 而int feet就是方法的参数.

上面例子的Main方法没有返回任何值, 所以它的返回类型是void.

C#里, Main方法是程序默认的入口. Main方法有时候返回类型是int(而不是void), 这样就可以返回一个值给运行环境(通常情况下, 非0值意味着发生了错误). Main方法还可以选择接收一个字符串数组作为参数(也就是所谓的命令行参数).

在程序的最外层, 类型是通过命名空间进行组织的. 使用using指令可以让System命名空间对我们的程序可见.

.NET Framework 的命名空间是嵌套组织的, 例如这个命名空间:

using System.Text;

不使用using的话, 通过完整的类名可以直接引用类:

System.Text.StringBuilder

编译

C#编译器把.cs文件编译成组件(assembly). 组件是.NET打包和部署的单元. 组件可以是程序或者库. 一个简单的控制台程序就是一个exe文件. 一个库就是一个dll, 它和exe基本一样, 只不过没有程序的入口. 它的作用是被别的库或者程序引用. .NET Framework就是一套库.

C#编译器是csc.exe. 你可以通过Visual Studio编译也可以从命令行手动调用csc来进行编译(这个编译器本身也是一个库).

例如:

csc MyFirstProgram.cs

将会产生一个名为MyFirstProgram.exe的程序.

想使用C#7的编译器, 就必须使用Visual Studio 2017 或 MSBuild 15 或通过dotnet cli

想要生成一个dll文件的话:

csc /target:library MyFirstProgram.cs

C#的语法

C#语法受到了 C和C++的启发

标识符和关键字

标识符就是类, 方法, 变量的名字, 程序员自己起的.

按约定, 本地变量, 私有字段 应该采用骆驼命名法camel case (例如myVariable).

其它的标识符都应该使用帕斯卡命名法Pascal Case (例如 MyMethod).

关键字就是编译器所识别和保留的特殊名字, 大多数关键字都不可以被用作标识符:

image.png

避免冲突

如果你非得使用关键字作为标识符的话, 就需要在你的标识符前边加上@符号:

class class {...} \ 非法
class @class {...} \ 合法

但是@并不是标识符的一部分, 所以@myVariable和myVariable是一样的.

下面这些关键子可以作为标识符, 而且不需要使用@前缀:

image.png

字面值, 标点符号 和 操作符

太简单了不介绍了.

注释

int x = 3; // 这是单行注释
int x = 3; /* 这是一个多行
                  的注释 */

类型

类型定义了值的蓝本.

int x = 12  * 30;

其中12 和 30这两个字面值的类型是int, x的类型也是int, x叫做变量.

变量代表着一个存储位置, 随着时间的变化, 这个位置可能含有不同的值.

与之相对的常量则表示不可变的值:

const int y =  355;

C#里面所有的值都是类型的实例.

预定义类型

int就是一个预定义类型, 它是整数类字面值的默认类型, 如果这个字面值不超过int的上下限的话.

string是另一个预定义类型, 表示字符串, 例如 "http://oreilly.com".

bool也是预定义类型, 它有两个值true, false.

还有很多预定义类型就不一一介绍了.

自定义类型

我们可以使用原始类型来组建复杂类型:

image.png

类型的成员

类型可以包括数据成员和函数成员(function members).

上面例子中 ratio 叫做字段(field), 它是UnitConverter的数据成员.

而它的函数成员有Convert方法和它的构造函数.

构造函数和初始化

数据是通过初始化一个类型得到的. 预定义类型可以使用字面值(例如 12, "Hello")直接进行初始化. 而new操作符可以创建一个自定义类型的实例:

UnitConverter feetToInchesConverter = new UnitConverter (12);

在使用new操作符初始化对象之后, 对象的构造函数就会被调用来执行初始化动作.

构造函数就像一个方法, 但是方法名和返回类型变成了类型的名:

image.png

实例成员 vs 静态成员

太简单了略....

静态类的所有成员必须是静态的. 例如Console类, 整个程序里面只会有一个Console.

public 关键字

略...

转换

C# 允许在兼容类型的实例间进行转换, 每次转换肯定会从当前的值创造出一个新的值.

转换分为显式或隐式:

int x = 12345;
long y = x; // 隐式
shortz = (short)x; // 显式

隐式转换的条件:

  • 编译器保证转换肯定会成功
  • 转换中没有信息的丢失 如果下列条件中的任意一个无法满足, 则需要使用显示转化:
  • 编译器不能保证转换成功
  • 转换中信息可能丢失.undefined(如果编译器认为转换肯定会失败, 那么这两种类型的转换都会被禁止)

值类型 vs 引用类型

所有的C#类型分为四种:

  • 值类型
  • 引用类型
  • 泛型类型参数
  • 指针类型

值类型包扩大多数内置的类型(所有的数值类型, char, bool), 还包括自定义的struct和enum(枚举)类型.

引用类型包括类, 数组, 委托 和接口 (也包括string类型).

这两种类型的区别就在于他们处理内存的方式.

值类型

值类型变量/常量的内容就是一个值, 例如int的内容就是32位的数据.

使用struct关键字可以创建自定义的值类型:

    public struct Point
    {
        public int X;
        public int Y;
    }

也可以简写:

    public struct Point
    {
        public int X, Y;
    }
内存中的值类型实例

为值类型赋值总是会复制实例, 例如:

image.png

下图表明p1和p2都有独自的存储空间:

赋值就是复制值类型的实例

引用类型

引用类型由两部分组成: 一个对象一个对象的引用. 引用类型变量/常量的内容其实是包含该值的那个对象的引用. 例如:

image.png
image.png

为引用类型赋值复制的是引用, 而不是那个对象的实例. 这就允许多个变量指向同一个对象, 这一点对于值类型来说就是不可能的. 下例中, 如果Point是类而不是struct:

image.png
p1, p2是指向同一个对象的两个引用

Null

一个引用可以被赋值为null, 表示该应用没有指向任何一个对象:

image.png

与之相对的, 值类型不可以为null:

image.png

存储开销

值类型的实例精确地占用了它的字段所需要的内存, 例如 Point就占用了8字节:

image.png

引用类型需要为引用和对象分配单独的内存. 这个对象除了需要它字段所用的内存外, 还需要额外的管理内存开销. 而每个对象的引用则需要额外的4或8字节, 这取决于.NET 是运行在32位还是64位平台上.

预定义类型分类

值类型:

  • 数值:
    • 有符号整型(sbyte, short, int, long)
    • 无符号整型(byte, ushort, uint, ulong)
    • 实数(float, double, decimal)
  • 逻辑 (bool)
  • 字符(char)

引用类型:

  • 字符串(string)
  • 对象(object)

C#里面的预定义类型其实是System命名空间下类型的别名, 例如下面两个语句只是语法不同而已:

image.png

在CLR里, 除了decimal之外的值类型都是原始类型. 之所以叫做原始类型是因为在编译后的代码里他们可以通过指令直接被支持, 通常会被翻译成CPU直接支持的东西:

image.png

此外, System.IntPtr 和 System.UIntPtr也是原始类型.

数值类型

C# 的预定义数值类型如下表:

image.png

整型的类型里面 int 和 long 是一等公民.

实数类型里面, float和double叫做浮点类型, 通常用在科学和图形计算. 而decimal类型通常用在财务金融方面, 因为这些领域需要基于10进制的高精度计算.

数值的字面值

数值的字面值可以使用10进制或者16进制来标记; 使用16进制表示的时候前边要加上0x:

int x = 123;
long y = 0x7F;

从C# 7开始, 你可以在数值的字面值里面加上下划线, 以增加可读性:

int million = 1_000_000;

使用二进制表示的时候, 前缀是0b:

var b = 0b1001_1101_0100_1100;

实数的字面值也可以使用小数或者指数:

double d = 1.5;
double million = 1E06;

数值字面值的类型推断

默认情况下, 编译器会从一个数值字面值推断出它是一个double类型还是一个整数类型.

  • 如果有小数或者有指数表示的符号E, 那么就是double
  • 否则, 该字面值的类型就是可以刚刚容纳该值的整型: int, uint, long, ulong.

数值字面值的后缀

数值字面值的后缀可以决定字面值的类型, 后缀可以是大写也可以是小写, 请看表:

image.png

后缀D其实可以去掉.

而F和M是最有用的后缀, 使用float或decimal的时候一定要加上.

数值转换

这部分大家都懂, 就不写了.

写一点需要注意的: 当你从浮点类型转换到整型的时候, 小数部分是被截断的, 没有进行舍入操作.

隐式的把一个很大的整型数转化为浮点类型的时候, 它的数量级是不变的, 但是有时会丢失精度.

例如:

int i1 = 100000001;
float f = i1;
int i2 = (int)f; // 100000000

decimal

decimal可以表示C#里的任意一个整型数值.

算术操作符

      • / %

自增自减操作符

++ --

整数类型的特殊操作

除法

整数除法只会取整数部分.

如果除数是0, 那么会抛出 DivideByZeroException

溢出

算出操作的溢出并不会抛出异常.

溢出检查操作符

checked 操作符会告诉运行时抛出异常.

checked 会影响包含++, --, +, - *, /操作符的表达式以及包含显式转换的表达式.

checked 可以用于表达式级或者语句块级:

int a = 1000000;
int b = 1000000;
int c = checked (a * b);
checked
{
    ...
    c = a * b;
    ...
}

如果你所有的表达式都做算术溢出检查的话, 可以使用 /checked+ 命令行开关(VS里面的Advanced Build Settings). 但这时如果有一部分代码你不想做溢出检查的话, 就可以使用unchecked操作符了:

int x = int.MaxValue;
int y = unchecked (x + 1);
unchecked
{
    int z = x + 1;
}

常量表达式的溢出检查

不管你是否设置了 /checke编译器开关, 在编译时算出的表达式总是进行溢出检查的话, 除非你使用unchecked操作符.

int x = int.MaxValue + 1; // 编译时错误
int y = unchecked (int.MaxValue + 1); // 没有错误

位操作符

image.png

8位和16位整数

它们是byte, sbyte, short, ushort.

它们没有自己的操作符, C#会在需要的时候隐式的对它们转换到大一点的类型.

特殊float和double值

特殊值: NaN(Not a Number), +∞, -∞, MaxValue, MinValue, Epsilon.

Console.WriteLine(double.NegativeInfinity); // -Infinity
image.png

用非零数除以0就会得到无限值:

1.0 / 0.0 // Infinity
-1.0 / 0.0 // -Infinity
1.0 / -0.0 //-Infinity
-1.0 / -0.0 // Infinity

用0除以0得到NaN. NaN不等于(==)任何值, 包括NaN.

判断是否为NaN:

float.IsNaN(xx), double.isNaN(xx).

但是使用object.Equals方法时, 两个NaN值是相等的.

double vs decimal

duoble适合科学计算.

decimal适合于金融财务计算.

对比:

image.png

实数的取舍错误

float和double实际上在内部是基于2来表示数值的. 所以只有能用基于2的形式来表达的数字才是准确的. 这也就意味着大部分含有小数的字面值(基于10的)表示起来都不太准确.

float tenth = 0.1f; // 其实不是0.1
float one = 1f;
Console.WriteLine(one - tenth * 10f); // -1.490116E-08

所以float和double不适合金融财务数值.

而decimal是基于10的, 所以它可以精确的表示基于10的数值.

Bool类型和操作符

bool (System.Boolean的别名).

比较操作符

==, !=, <, >, >=, <=

条件操作符

&&, ||, 这俩是短路求值, 操作符前边的表达式符合要求的话, 后边的表达式就不执行了.

与之相对的是 &, |, 这俩不是短路的, 操作符两边的表达式都会被执行. 但是不怎么用他俩.

!, 取反.

三目操作符

var c = (a > b) ? a : b;

字符串和字符

char (System.Char的别名), 字符, 它表示一个Unicode字符, 占两个字节. 字面值使用单引号.

var c = 'A';

还有转义字符:

image.png

使用u或x, 可以通过四位16进制的形式表示任何一个Unicode字符:

char copyrightSymbol = 'u00A9';

char的转型

char可以隐式的转换为数值型, 但是要求该数值型至少可以容纳.无符号short的大小. 针对其它数值类型需要显式转换.

字符串

string (System.String), 表示了一串不可变的Unicode字符.

string是引用类型, 但是在使用==比较的时候, 比较的是值:

string a = "test";
string b = "test";
Console.WriteLine(a == b); // True

C# 允许逐字字符串, 使用符号 @, 但是不支持转义字符. 下面两个是相同的:

string a1 = "\\server\ab.cs";
string a2 = @"\serverab.cs";

逐字字符串支持多行:

string verbatim = @"First Line
Second Line";

在逐字字符串里面显式双引号, 需要把双引号写两遍:

string xml =@"<h1 id=""title""></h1>";

字符串连接

可以用+连接字符串, 但是效率低. 应该用StringBuilder.

针对非字符串类型的变量使用+时, 会自动调用其的ToString()方法:

string s = "a" + 5; //a5

字符串插值

在字符串前边使用 $ 符号就是插值的字符串. 下面这个就是插值字符串:

int x = 4;
Console.WriteLine($"A square has {x} sides"); // A square has 4 sides

其实也是调用了ToString()或等效的方法.

可以在表达式后边加上冒号和格式字符串来对其进行格式化:

string s = $"255 in hex is {byte.MaxValue:X2}"; // X2 表示两位16进制
// 255 in hex is FF

插值字符串只能是单行的, 除非结合@一起用, 但是$必须在@前边:

int x = 2;
string s = $@"this spans {
    x} lines";

在插值字符串里面显式括号的话需要输入两遍.

字符串比较

字符串不支持 >, <操作符进行比较. 需要使用string的CompareTo方法.