Net Core教程

《c#10 in a nutshell》--- 读书随记(2)

本文主要是介绍《c#10 in a nutshell》--- 读书随记(2),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Chaptor 1 . C# Language Basics

内容来自书籍《C# 10 in a Nutshell》
Author:Joseph Albahari
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的

A First C# Program

int x = 12 * 30;
System.Console.WriteLine (x);

计算 12 * 30的结果,然后存储在变量x中,因为C#是强类型的语言,所以所有变量的类型都必须是编译期间已知

然后Console类的静态方法WriteLine,前面的System是命名空间,我们也可以用using System来导入这个命名空间下的所有公开的API,这样就不需要频繁编写前缀了Console.WriteLine (x);

using System;

Console.WriteLine (FeetToInches (100));

int FeetToInches (int feet)
{
    int inches = feet * 12;
    return inches;
}

方法的声明,前面是返回值类型,方法名,入参的形参变量类型和名称。方法体是用一个大括号包裹着。和Java的不一样,它的大括号是都在方法签名的下方,Java的是有一个大括号在方法签名的末尾。如果方法没有返回值,那么应该使用void类型,代表这个方法没有返回值

Compilation

C#的编译器会将源码编译到assembly.,一个assembly是包的集合。它可以是一个应用或者库,它们之间的不同是,应用是有一个入口的执行函数,但是库没有入口。

库的目的是被应用所引用或者被其他库引用。

每个程序的的都有一个被叫做top-level statements的入口文件,这个文件会隐式创建一个入口函数,也就是常说的Main函数

dotnet命令行工具,是可以用来管理.NET源码和二进制程序的,它可以构建程序,运行程序

// 创建一个Console程序
dotnet new Console -n MyFirstProgram

// 构建程序,然后运行这个程序
dotnet run MyFirstProgram

// 构建这个程序
dotnet build MyFirstProgram.csproj

需要注意的是,在执行dotnet命令的时候,最好处于项目内部

Syntax

Identifiers and Keywords

using System;
int x = 12 * 30;
Console.WriteLine (x);

System, x, Console, WriteLine,都是标识符,是开发者用来选择类、方法、变量等

标识符可以使用下划线、字母开头的Unicode字符集,区分大小写,按照约定,参数、变量和私有字段应该用camel case,而其他的所有标识符应该用Pascal case

Keywords关键字,指的是对编译器来说,是有特殊含义的一些单词,这里使用到了using, int,要注意的是,不能使用关键字作为标识符

如果真的希望使用关键字作为标识符,可以添加@前缀给标识符

Comments

单行注释//

多行注释/* ... */

文档注释///

Type Basics

在C#中,所有的值都是类型的实例

Predefined Type Examples

预定义类型是编译器提供的特殊的类型。比如说基本数字类型int,还有字符串类型string,布尔值bool

Custom Types

public class UnitConverter
{
    int ratio;      // Field 字段

    public UnitConverter(int unitRatio)  // Constructor 构造函数
    {
        ratio = unitRatio;
    }

    public int Convert(int unit)    // Method   方法
    {
        return unit * ratio;
    }
}

The public keyword

如果标注了public关键字,会将这个类型的成员暴露出来,如果没有标注访问权限,默认是private的权限

Defining namespaces

namespace A 
{
    public class B
    {
        ...
    }
}

类型B的命名空间在A,在调用B的时候,需要带上new A.B(),才能使用

Types and Conversions

两个合适的类型可以进行转换,可以进行隐式或者显式的自动转换

int x = 12345;
long y = x; // Implicit
short z = (short)x;  // Explicit

Implicit的转换会发生在两种情况:

  • 编译器可以保证这个转换总是成功的
  • 没有信息会在转换中丢失

Explicit的转换只需要其中一种:

  • 编译器不能保证转换是成功
  • 信息可能在转换中丢失

Value Types Versus Reference Types

所有的c# 类型都可以区分为下面的几种类别中:

  • Value types 值类型
  • Reference types 引用类型
  • Pointer types 指针类型
  • Generic type parameters 范型类型参数

值类型一般是内建的基础类型,比如那些数字类型intlong,还有字符类型char等,还有就是我们自定义的struct类型和enum类型也算是值类型

引用类型包括所有的classarraydelegateinterface类型(包括string也是引用类型)

值类型和引用类型的不同之处在,它们处理内存的方式不同

Value types

值类型或者常量的内容,只是一个简单的值,比如int内置类型,它是一个32bit的数据,也可以自定义struct类型

public struct Point 
{ 
    public int X, Y; 
}

它的内存视图:

值类型的实例在赋值的时候,总是copy的行为

Point p1 = new Point();
p1.X = 7;
Point p2 = p1;// Assignment causes copy
p1.X = 9;

Reference types

引用类型要更加复杂,它有两个部分组成一个是对象,一个是指向对象的引用。

它的内存视图

在对引用类型做赋值动作的时候,只会cpoy引用,而不是对象实例。这样就造成,可以同时有多个引用变量指向同一个对象。

Point p1 = new Point();
p1.X = 7;
Point p2 = p1;// Copies p1 reference
p1.X = 9;// Change p1.X

Null

一个引用类型,可以被赋值为null,表明这个引用没有指向任何对象

Storage overhead

值类型占用的内存大小是刚刚好的,比如说结构体Point,它的占用大小是8bytes

struct Point
{
    int x; // 4 bytes
    int y; // 4 bytes
}

需要注意的是,这个大小是会有内存对齐的原因,而导致和你预想的不一样

struct A { byte b; long l; }

这个看起来可能是占用7bytes,但是实际是16bytes,因为会对齐最大的那个字段的大小

Specialized Operations on Integral Types

Overflow check operators

checked操作符可以在运行时生成一个错误OverflowException,这样比变量溢出了,但是没有任何反馈。

int c = checked (a * b);// 只是检查这个表达式

// 检查block内的代码
checked
{
    ...
    c = a * b;
    ...
}

也可以解除这种检查,通过使用关键字unchecked

关于常量的计算溢出,是在编译时做检查,所以不会隐式溢出

8- and 16-Bit Integral Types

8bit和16bit的整数类型,有byte、sbyte、short和ushort,这些类型的计算操作,C#会隐式地转换为更大的类型,而这会造成一个编译时错误

short x = 1, y = 1;
short z = x + y;    // Compile-time error

short z = (short) (x + y);  // OK

Strings and Characters

C#的char类型代表了一个Unicode字符,占用2bytes(UTF-16)

String Type

C#的一个字符串代表一个不可变的Unicode字符序列

在字符串的前面添加@,代表字符串的内容不做转义

String interpolation

插值字符串

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

Arrays

char[] vowels = new char[5];    // 声明一个字符数组

char[] vowels = new char[] {'a','e','i','o','u'};   声明并初始化一个字符数组

char[] vowels = {'a','e','i','o','u'};

Default Element Initialization

创建一个数组,总是会预初始化数组元素为默认值。默认值在元素是值类型或者引用类型是有性能影响的。

如果元素是值类型,那么每个元素都会作为元素的一部分初始化

Point[] a = new Point[1000];
int x = a[500].X;   // 0
public struct Point { public int X, Y; }

如果是引用类型,那么元素会是null

Point[] a = new Point[1000];
int x = a[500].X;   // Runtime error, NullReferenceException
public class Point { public int X, Y; }

Indices and Ranges

Indices

索引可以让你引用一个相对数组的末尾的元素,使用^操作符。比如,^1就是获取到数组的最后一个元素,^2是数组的倒数第二个元素

char[] vowels = new char[] {'a','e','i','o','u'};
char lastElement = vowels [^1];     // 'u'
char secondToLast = vowels [^2];    // 'o'

这个操作符其实结果是一个Index类型

Index first = 0;
Index last = ^1;
char firstElement = vowels [first];     // 'a'
char lastElement = vowels [last];       // 'u'

Ranges

Ranges让你可以对一个数组切片,用..操作符

char[] firstTwo = vowels [..2];     // 'a', 'e'
char[] lastThree = vowels [2..];    // 'i', 'o', 'u'
char[] middleOne = vowels [2..3];   // 'i'

操作符的结果是一个类型Range

Range firstTwoRange = 0..2;
char[] firstTwo = vowels [firstTwoRange];   // 'a', 'e'

Multidimensional Arrays

多维数组有两种形式: rectangularjagged

矩形数组表示 n 维内存块,锯齿数组是数组的数组。

Rectangular arrays

int[,] matrix = new int[3,3];

int[,] matrix = new int[,]
{
    {0,1,2},
    {3,4,5},
    {6,7,8}
};

Jagged arrays

int[][] matrix = new int[3][];

int[][] matrix = new int[][]
{
    new int[] {0,1,2},
    new int[] {3,4,5},
    new int[] {6,7,8,9}
};

它是数组的数组,所以在声明的时候,可以声明第一个数组,内部元素都是null,然后内部的数组可以每一个的长度都不一致

Simplified Array Initialization Expressions

char[] vowels = {'a','e','i','o','u'};

int[,] rectangularMatrix =
{
    {0,1,2},
    {3,4,5},
    {6,7,8}
};

int[][] jaggedMatrix =
{
    new int[] {0,1,2},
    new int[] {3,4,5},
    new int[] {6,7,8,9}
};

var vowels = new[] {'a','e','i','o','u'};

var rectMatrix = new int[,]
{
    {0,1,2},
    {3,4,5},
    {6,7,8}
};

var jaggedMat = new int[][]
{
    new int[] {0,1,2},
    new int[] {3,4,5},
    new int[] {6,7,8,9}
};

Bounds Checking

在运行时会对数组做边界检查

Variables and Parameters

一个变量代表的是一个可变的存储位置,它可以是一个本地变量、参数(value,ref,out,in)、字段(实例、静态),或者是一个数组的元素

The Stack and the Heap

栈和堆是存储变量的地方,它们有着不同的生命周期

Stack

栈是用来存储本地变量和参数的。随着方法或者函数的进入和退出,栈会逻辑地增长和收缩。

Heap

堆是对象(即是引用类型)的驻留内存。每当一个对象创建,就会在堆中申请内存,并有一个引用指向这个对象。运行时有垃圾收集器来将对象的占用内存释放。当对象不再被引用之后,就会被垃圾收集器回收

Default Values

可以用关键字default获取任何类型的默认值

Console.WriteLine (default (decimal));

decimal d = default;

Parameters

可以控制参数传递的额外的行为

Passing arguments by value

在默认情况下,C#是值传递,这意味着会copy一个值传递到方法内部,所以在方法的内部,对参数的修改,不会影响到方法外部的变量;但是如果将一个引用使用值传递的方式传递到方法,那么会变成两个变量指向一个对象,所以在方法内部对这个对象的修改,会影响到外部的变量,而如果是给这个引用参数赋值,其实是将这个引用指向其他的对象,对外部变量没有影响。

int x = 8;
Foo (x);

Console.WriteLine (x);

static void Foo (int p)
{
    p = p + 1;
    Console.WriteLine (p);
}
StringBuilder sb = new StringBuilder();
Foo (sb);
Console.WriteLine (sb.ToString());  // test

static void Foo (StringBuilder fooSB)
{
    fooSB.Append ("test");
    fooSB = null;
}

The ref modifier

参数传递的时候,添加了ref关键字在方法签名中,这相当于给实参起了一个别名,它们同时指向同一个内存地址,或者说一个内存地址有两个变量名。注意的是,ref是传递方和声明方都要写

int x = 8;
Foo (ref x);    // Ask Foo to deal directly with x
Console.WriteLine (x);  // x is now 9


static void Foo (ref int p)
{
    p = p + 1;  // Increment p by 1
    Console.WriteLine (p);  // Write p to screen
}

The out modifier

out关键字和ref差不多,除了:

  • 在进入函数之前,它不需要被赋值
  • 在函数返回之前,必须被赋值
Split ("Stevie Ray Vaughan", out string a, out string b);

void Split (string name, out string firstNames, out string lastName)
{
    int i = name.LastIndexOf (' ');
    firstNames = name.Substring (0, i);
    lastName = name.Substring (i + 1);
}

The in modifier

in关键字也是和ref差不多,唯一区别是,in修饰的参数,在方法中是不可变的,如果改了,会有编译错误。

The params modifier

params关键字是用在方法的最后一个参数的修饰符。它允许方法接收指定类型的多个参数。参数的类型必须是数组形式的

int total = Sum (1, 2, 3, 4);

int Sum (params int[] ints)
{
    int sum = 0;
    for (int i = 0; i < ints.Length; i++)
        sum += ints [i];
    return sum;
}

Optional parameters

可选参数,在方法声明中已经给定某个值

Foo();
Foo (23);

void Foo (int x = 23) { Console.WriteLine (x); }

可选参数,不可以用ref或者out修饰符

Named arguments

Foo (x:1, y:2);     // 1, 2

void Foo (int x, int y) { Console.WriteLine (x + ", " + y); }

Ref Locals

int[] numbers = { 0, 1, 2, 3, 4 };
ref int numRef = ref numbers [2];

numRef是numbers[2]的引用,修改numRef,会影响到numbers[2]

而且,Ref locals只能应用在数组元素、字段、或者本地变量,不能应用在属性上

一般是和ref returns一起使用

Ref Returns

可以在方法中返回一个Ref locals,这就是ref returns

static string x = "Old Value";
static ref string GetX() => ref x;

static void Main()
{
    ref string xRef = ref GetX();
    xRef = "New Value";
    Console.WriteLine (x);`
}
// 这也是合法的,只是方法返回值不是ref的
string localX = GetX();     // Legal: localX is an ordinary non-ref variable.
// 可以在返回值添加只读限制
static ref readonly string Prop => ref x;

Target-Typed new Expressions

System.Text.StringBuilder sb1 = new();
System.Text.StringBuilder sb2 = new ("Test");

等价于

System.Text.StringBuilder sb1 = new System.Text.StringBuilder();
System.Text.StringBuilder sb2 = new System.Text.StringBuilder ("Test");

Expressions and Operators

Operator Table




Null Operators

C#提供了三种操作符让操作null值更简单:null-coalescingnull-coalescing assignmentnull-conditional

Null-Coalescing Operator

??操作符。语义是“如果左边的操作数不是null,将它给我;否则,给我另外一个值(右操作数)”

string s1 = null;
string s2 = s1 ?? "nothing";    // s2 evaluates to "nothing"

它同样适用于nullable value types,可空值类型

Null-Coalescing Assignment Operator

??=操作符。它的语义是“如果左操作数是null,那就将右操作数赋值给左操作数”

myVariable ??= someDefault;

这个操作符特别适用于延迟加载的属性

Null-Conditional Operator

?.操作符。它允许你在调用方法或者访问成员时,和正常的dot操作符一样,除了当你的左操作数是null的时候,整个表达式的结果是null,而不是抛出异常NullReferenceException

System.Text.StringBuilder sb = null;
string s = sb?.ToString();  // No error; s instead evaluates to null

同样还有个索引访问的时候,也可以使用?[]

Statements

Selection Statements

The switch statement

switch语句,语句是没有结果的

switch (cardNumber)
{
    case 13:
        Console.WriteLine ("King");
        break;
    case 12:
        Console.WriteLine ("Queen");
        break;
    case 11:
        Console.WriteLine ("Jack");
        break;
    default:
        Console.WriteLine (cardNumber);
        break;
}

还可以匹配类型

switch (x)
{
    case int i:
        Console.WriteLine ("It's an int!");
        Console.WriteLine ($"The square of {i} is {i * i}");
        break;
    case string s:
        Console.WriteLine ("It's a string");
        Console.WriteLine ($"The length of {s} is {s.Length}");
        break;
    default:
        Console.WriteLine ("I don't know what x is");
        break;
}

Switch expressions

switch表达式,是有计算结果的,可以作为右值赋值给左值

string cardName = cardNumber switch
{
    13 => "King",
    12 => "Queen",
    11 => "Jack",
    _ => "Pip card" // equivalent to 'default'
};

Iteration Statements

foreach loops

对一个enumerable对象迭代。

foreach (char c in "beer")
    Console.WriteLine (c);

Miscellaneous Statements

using语句,为IDisposable对象在finallyblock里面调用Dispose方法

lock语句是一个是调用 Monitor 类的 Enter 和 Exit 方法的快捷方式

Namespaces

命名空间是类型名称的域。类型通常组织为分层命名空间,这样容易防止命名冲突

类型就包裹在namespaceblock下

namespace Outer.Middle.Inner
{
    class Class1 {}
    class Class2 {}
}

等价于

namespace Outer
{
    namespace Middle
    {
        namespace Inner
        {
            class Class1 {}
            class Class2 {}
        }
    }
}

using static

导入了这个类型的所有公开静态成员

Rules Within a Namespace

声明在外部的命名空间,可以不需要导入就能让它的内部的命名空间访问

namespace Outer
{
    class Class1 {}

    namespace Inner
    {
        class Class2 : Class1
        {}
    }
}

Aliasing Types and Namespaces

起别名

using PropertyInfo2 = System.Reflection.PropertyInfo;

Advanced Namespace Features

Extern

当程序引用的两个库,都有相同的命名空间和类型名称时,可通过以下方式解决

<ItemGroup>
    <Reference Include="Widgets1">
        <Aliases>W1</Aliases>
    </Reference>
    <Reference Include="Widgets2">
        <Aliases>W2</Aliases>
    </Reference>
</ItemGroup>
extern alias W1;
extern alias W2;
W1.Widgets.Widget w1 = new W1.Widgets.Widget();
W2.Widgets.Widget w2 = new W2.Widgets.Widget();
这篇关于《c#10 in a nutshell》--- 读书随记(2)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!