深入理解C#(第3版)-- 【C#4】第13章 简化代码的微小修改(学习笔记)

13.1   可选参数(optional parameter)和命名实参(named argument)

13.1.1   可选参数

1. 动机

如果某个操作需要多个值,而有些值在每次调用的时候又往往是相同的,这时通常可以使用可选参数。

2. 声明可选参数并在调用时省略它们

创建可选参数是非常简单的,只需使用类似变量初始化程序一样的语法提供一个默认值。

注意,尽管编译器可以对可选参数和实参进行一些智能分析,来确定省略的参数类型,但它并没有这么做:它假设我们提供的实参顺序与参数定义的顺序是一样的。

3. 可选参数的约束

可选参数包含一些规则。所有可选参数必须出现在必备参数之后,参数数组(用params 修饰符声明)除外,但它们必须出现在参数列表的最后,在它们之前为可选参数。参数数组不能声明为可选的,如果调用者没有指定值,将使用空数组代替。可选参数还不能使用ref 或out 修饰符。

可选参数可以为任何类型,但对于指定的默认值却有一些限制。它们必须为常量:数字或字符串字面量、null、const成员、枚举成员和default(T)操作符。此外对于值类型,你还可以调用与default(...)操作符等价的无参构造函数。指定的值会隐式转换为参数类型,但这种转换不能是用户定义的。

表13-1   使用可选参数的一些有效的方法参数列表


 声明                 已构造类型的例子


Foo(int x, int y = 10)           默认值采用数字字面量
Foo(decimal x = 10)           从int 到decimal的隐式内置转换
Foo(string name = "default")        默认值采用字符串字面量
Foo(DateTime dt = new DateTime())   DateTime 的零值
Foo(DateTime dt = default(DateTime))  另一种零值语法
Foo< T >(T value = default(T))      类型参数的默认值操作符
Foo(int? x = null)            可空转换
Foo(int x, int y = 10, params int[] z)    在可选参数之后的参数数组


 表13-2   使用可选参数的一些无效的方法参数列表


 声明                 已构造类型的例子


 Foo(int x = 0, int y)            非参数数组的必备参数出现在可选参数之后 

Foo(DateTime dt = DateTime.Now)     默认值必须为常量
Foo(XName name = "default")       从string到XName 的转换是用户定义的
Foo(params string[] names = null)      参数数组不能为可选的
Foo(ref string name = "default")       ref/out参数不能为可选的


 4. 版本化和可选参数

对可选参数默认值的约束非常类似对const 字段和特性值的约束。在这两种情况下,当编译器引用这些值时,会直接将其复制到输出结果中。所生成的IL 与源代码中包含默认值时是完全一致的。这意味着如果改变默认值而不重新编译引用它的代码,那么这些代码仍将使用旧的默认值。

具体来说,可遵循以下步骤。
(1) 创建包含类似下面这个类的类库(Library.dll):

public classLibraryDemo
{
    public static void PrintValue(int value = 10)
    {
        System.Console.WriteLine(value);
    }
}

 (2) 创建引用该类库的控制台应用程序(Application.exe):

public classProgram
{
    static void Main()
    {
        LibraryDemo.PrintValue();
    }
}

(3) 运行应用程序,如果不出意外将打印10。 

(4) 像下面这样更改PrintValue的声明,然后只重新编译类库:

public static void PrintValue(int value = 20)

(5) 再次运行应用程序,仍然打印10。该值已经被直接编译到了可执行文件里。 

(6) 重新编译应用程序,然后运行,这次将打印20。

5. 用可空性使默认值更加灵活

幸运的是,我们还可以突破默认值必须为常量的限制。我们引入一个魔值来表示默认值,然后在方法内部用真正的默认值替换这个魔值,使用null来作为魔值。

如果参数类型为值类型,我们只需使用相应的可空值类型,这时仍然可以将默认值指定为null。 

代码清单13-2   使用空默认值来处理非常量的情况

static void AppendTimestamp(string filename,
                            string message,
                            Encoding encoding = null,
                            DateTime? timestamp = null)
{
    Encoding realEncoding = encoding ?? Encoding.UTF8;
    DateTime realTimestamp = timestamp ?? DateTime.Now;
    using(TextWriter writer = new StreamWriter(filename,
            true,
            realEncoding))
    {
        writer.WriteLine("{0:s}: {1}", realTimestamp, message);
    }
}

...
AppendTimestamp("utf8.txt", "First message");
AppendTimestamp("ascii.txt", "ASCII", Encoding.ASCII);
AppendTimestamp("utf8.txt", "Message in the future", null,
                new DateTime(2030, 1, 1));

13.1.2   命名实参

命名实参的基本概念是,在指定实参的值时,可以同时指定相应参数的名称。编译器将判断参数的名称是否正确,并将指定的值赋给这个参数。

1. 语法

当然,最常犯的错误是把参数顺序弄反。如果没有命名参数,就会产生问题:消息框中的标题和内容是反的。但有了命名参数,参数顺序就是无关紧要的了。

包含out 和ref 的命名实参 

如果要对包含ref 或out 的参数指定名称,需要将ref 或out 修饰符放在名称之后,实参之前。如对于int.TrayParse 来说,代码如下:

int number; 
bool success = int.TryParse("10", result: out number); 

代码清单13-3   使用命名实参的简单示例

static void Dump(int x, int y, int z)
{
    Console.WriteLine("x={0} y={1}z={2}", x, y, z);
}

...
Dump(1, 2, 3);
Dump(x: 1, y: 2, z: 3);
Dump(z: 3, y: 2, x: 1);
Dump(1, y: 2, z: 3);
Dump(1, z: 3, y: 2);

所有命名实参都必须位于位置实参之后,两者之间的位置不能改变。位置实参总是指向方法声明中相应的参数——你不能跳过参数之后,再通过命名相应位置的实参来指定。也就是说下面这两个调用都是无效的。
 Dump(z: 3, 1, y: 2)——位置实参必须在命名实参之前。
 Dump(2, x: 1, z: 3)——x已经由第一个位置实参指定了,因此不能再用命名实参指定。

2. 实参求值顺序

实参仍然按编写顺序求值,即使这个顺序有可能会不同于参数的声明顺序。

代码清单13-4   记录实参求值

static int Log(int value)
{
    Console.WriteLine("Log: {0}", value);
    return value;
}

...
Dump(x: Log(1), y: Log(2), z: Log(3));
Dump(z: Log(3), x: Log(1), y: Log(2));

13.1.3   两者相结合

1. 不易变性和对象初始化 

不易变类型是函数式编程的核心部分

2. 重载决策

为了检查是否存在特定的适用方法,编译器会使用位置参数的顺序构建一个传入实参的列表,然后对命名实参和剩余的参数进行匹配。如果没有指定某个必备参数,或某个命名实参不能与剩余的参数相匹配,那么这个方法就不是适用的。

首先,如果两个方法均为适用的,其中一个方法的所有实参都显式指定,而另一个方法使用了某个可选参数的默认值,则未使用默认值的方法胜出。但这并不适用于仅比较所使用的默认值数量这种情况——它是严格按照“是否使用了默认值”来划分的。

static void Foo(int x = 10) {}
static void Foo(int x = 10, int y = 20) {}

...
Foo(); //错误:会引起歧义
Foo(1); //调用第一个重载
Foo(y: 2); //调用第二个重载
Foo(1, 2); //调用第二个重载

其次,命名实参有时候可以代替强制转换,来辅助编译器进行重载决策。如果两个不同的方法都能将实参转换为参数类型,并且哪种方法都不比另一种方法更好,这时就会引起歧义。

void Method(int x, object y) { ... }
void Method(object a, int b) { ... }

...
Method(10, 10); //有歧义的调用

两个方法都是适用的,但哪一个都不比另一个更优。如果不想更改方法名称以消除歧义的话,那么有两种方式可以解决这个问题。(改名是我的首选方案。明确而详实的方法名称可以提升代码的可读性。)

void Method(int x, object y) { ... }
void Method(object a, int b) { ... }

...
Method(10, (object) 10);//通过强制转换消除歧义
Method(x: 10, y: 10); //通过命名实参消除歧义

13.2  改善COM(Component Object Model)互操作性

13.2.1  在C# 4之前操纵Word是十分恐怖的

13.2.2   可选参数和命名实参的复仇

13.2.3   按值传递ref 参数

13.2.4   调用命名索引器

13.2.5   链接主互操作程序集

13.3   接口和委托的泛型可变性

13.3.1   可变性的种类:协变性和逆变性

实质上,可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用。

可变性应用于泛型接口和泛型委托的类型参数中。

可变性有两种类型:协变性和逆变性。二者概念基本相同,只是在上下文中转换的方向不同。

1. 协变性:从API返回的值

协变性用于向调用者返回某项操作的值。

interface IFactory<T>
{
    T CreateInstance();
}

2. 逆变性:传入API的值

逆变性则相反。它指的是调用者向API传入值,即API是在消费值,而不是产生值。

interface IPrettyPrinter<T>
{
    void Print(T document);
}

3. 不变性:双向传递的值

如果协变性适用于仅从API输出值的情况,而逆变性用于仅向API输入值的情况,那么如果值双向传递会如何呢?简而言之,什么也不会发生。这种类型是不变体(invariant )。  

interface IStorage<T>
{
    byte[] Serialize(T value);
    T Deserialize(byte[] data);
}

13.3.2   在接口中使用可变性

在泛型接口或委托的声明中,C# 4 能够使用out 修饰符来指定类型参数的协变性,使用in修饰符来指定逆变性。

变体的转换是引用转换 

任何使用了协变和逆变的转换都是引用转换,这意味着转换之后将返回相同的引用。它不会创建新的对象,只是认为现有引用与目标类型匹配。这与在某个层次结构中,引用类型之间的强制转换是相同的.

1. 用in和out 表示可变性

IEnumerable<T>(对于T是协变的)和IComparer<T>(对于T是逆变的),以下是它们在.NET 4 中的声明:

public interface IEnumerable<out T>
public interface IComparer<in T>
List<Circle>  circles = new List<Circle>
{
    new Circle(new Point(0, 0), 15),
    new Circle(new Point(10, 5), 20),
};

List<Square> squares = new List<Square>
{
    new Square(new Point(5, 10), 5),
    new Square(new Point(-10, 0), 2)
};

2. 接口的协变性

代码清单13-12  根据圆形和方形列表构建一般形状的列表

List<IShape> shapesByAdding = new List<IShape>();
shapesByAdding.AddRange(circles);
shapesByAdding.AddRange(squares);
List<IShape> shapesByConcat = circles.Concat<IShape>(squares).ToList();

对于LINQ to Objects来说,协变性尤其重要,因为很多API都表示为IEnumerable<T>;

3. 接口的逆变性

13.3.3   在委托中使用可变性

delegate T Func<out T>()
delegate void Action<in T>(T obj)

代码清单13-14 用 Func<T>和Action<T>委托演示可变性

Func<Square> squareFactory = () => new Square(new Point(5, 5), 10);
Func<IShape> shapeFactory = squareFactory;

Action<IShape> shapePrinter = shape => Console.WriteLine(shape.Area);
Action<Square> squarePrinter = shapePrinter;

squarePrinter(squareFactory());
shapePrinter(shapeFactory());

13.3.4   复杂情况

1. Converter<TInput, TOutput> :同时使用协变性和逆变性

.NET 2.0 就已经包含了Converter<TInput, TOutput>委托类型。它与Func<T, TResult>是等效的,但意图更加明确。在.NET 4 中,它变成了Converter<in TInput, out TOutput>,展示了哪个类型参数使用了哪种可变性。

2. 疯狂的高阶函数

http://mng.bz/79d8

http://mng.bz/94H3

13.3.5   限制和说明

1. 不支持类的类型参数的可变性

只有接口和委托可以拥有可变的类型参数。

2. 可变性只支持引用转换

3. out 参数不是输出参数

delegate bool TryParser<T>(string input, out T value)

4. 可变性必须显式指定

5. 注意破坏性修改

6. 多播委托与可变性不能混用

Func<string> stringFunc = () => "";
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + stringFunc;
Func<string> stringFunc = () => "";
Func<object> defensiveCopy = new Func<object>(stringFunc);
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + defensiveCopy;

7. 不存在调用者指定的可变性,也不存在部分可变性

Java Generics FAQ

13.4   对锁和字段风格的事件的微小改变

13.4.1   健壮的锁

如果线程在获取锁之后和进入try 块之前异常终止,我们也无法释放锁。

.NET 4 新增加了Monitor.Enter 的重载,C# 4 的编译器将使用这种方式:

bool acquired = false;
object tmp = listLock;
try
{
    Monitor.Enter(tmp, ref acquired);
    list.Add("item");
}
finally
{
    if (acquired)
    {
        Monitor.Release(tmp);
    }
}

13.4.2   字段风格的事件

字段风格的事件像字段一样进行声明,不再包含显式的add/remove语句块,如下:

public event EventHandler Click;

首先,线程安全的实现方式发生了改变。

其次,在声明事件的类中,事件名称的含义改变了。

http://mng.bz/Kyr4

posted @ 2019-10-18 13:21  FH1004322  阅读(243)  评论(0)    收藏  举报