从零开始独立游戏开发学习笔记(十七)--Unity学习笔记(六)--微软C#指南(三)

507 阅读4分钟

好了,忙的时候结束了。

继续讲述和对象相关的知识。这一章讲使用模式匹配进行类型转换。

1. 如何安全地格式转换(模式匹配)

由于对象具有多态性。一个具有基类类型的变量是可以存放 derived 类型的变量的值的,但这有可能产生 InvallidCastException。C# 提供了使用模式匹配(pattern match)的格式转换(cast),仅当成功的时候会转换。C# 也提供了 isas 关键字来判断一个值是否为某个类型。

1.1 is 运算符

比如说以下代码:

static void FeedMammal(Animal a) {
    if (a is Mammmal m)
    {
        m.Eat();
    }
    else 
    {
        Console.WriteLine($"{a.GetType().Name} is not a Mammal");
    }
}

重点在于:

  1. is 后面并不只是一个类型,而是声明了一个 Mammal 类型的变量。并不是说只能这么写。单单写 a is Mammal 也行,只是这种语法把类型判断和初始化写在一起,也是可行的一种语法。当判断成功的时候,a 的值会被赋予给了 m。
  2. m 的作用域仅仅在于 if 里,甚至连 else 里都无法访问。

1.2 as 运算符

请看以下代码:

static void TestForMammals(Object o) {
    var m = o as Mammal;
    if (m != null)
    {
        Console.WriteLine(m.ToString());
    }
    else
    {
        Console.WriteLine($"{o.GetType().Name} is not a Mammal");
    }
}
  1. as 运算符执行一次转换。如果成功则转换成对应类型,不成功则返回 null。
  2. 顺便一提,上面的 m != null 也可以换成 m is not null

1.3 switch 做类型匹配

如下所示的语法也是可以的:

static void PatternMatchingSwitch(System.ValueType val)
{
    switch (val)
    {
        case int number:
            Console.WriteLine(number);
            break;
        case long number:
            Console.WriteLine(number);
            break;
        case decimal number:
            Console.WriteLine(number);
            break;
        case float number:
            Console.WriteLine(number);
            break;
        case double number:
            Console.WriteLine(number);
            break;
        case null:
            Console.WriteLine("val is a nullable type with the null value");
            break;
        default:
            Console.WriteLine("Could not convert " + val.ToString());
            break;
    }
}

2. 模式匹配的场景

现代开发经常要用到来自各种不同地方的数据源,因此数据类型也都不一致。
于是文章采用了这么一个场景--在一个收费站收费。根据高峰期和车型收费。 难点在于,数据来源可能是多个不同的外部系统。那么首先假设有这么三个系统(3 个 namespace):

namespace ConsumerVehicleRegistration
{
    public class Car
    {
        public int Passengers { get; set; }
    }
}

namespace CommercialRegistration
{
    public class DeliveryTruck
    {
        public int GrossWeightClass { get; set; }
    }
}

namespace LiveryRegistration
{
    public class Taxi
    {
        public int Fares { get; set; }
    }

    public class Bus
    {
        public int Capacity { get; set; }
        public int Riders { get; set; }
    }
}

即,数据可能以不同的 class 形式存在。

2.1 最基础的收费

写一个最基础的收费类:

using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;

namespace toll_calculator
{
    public class TollCalculator
    {
        public decimal CalculateToll(object vehicle) =>
            vehicle switch
        {
            Car c           => 2.00m,
            Taxi t          => 3.50m,
            Bus b           => 5.00m,
            DeliveryTruck t => 10.00m,
            { }             => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
            null            => throw new ArgumentNullException(nameof(vehicle))
        };
    }
}

这里使用了一个 switch expression 的语法(非 switch statement)。语法一看大概也知道是怎么回事。因为整个是一个 switch,因此 => 跟的就是 return 的值。

  1. { } 则是匹配所有的 非 null 的 object。必须写在后面,否则就被第一个返回了。
  2. null 则是匹配 null。

2.2 根据乘客收费

为了减少流量,让车辆载客数更高,因此希望乘客越少收费越高。

我们可以改写上面的代码:

public class TollCalculator
    {
        public decimal CalculateToll(object vehicle) =>
            vehicle switch
        {
            Car {Passengers: 0} => 2.00m + 0.50m, 
            Car {Passengers: 1} => 2.0m,
            Car {Passengers: 2} => 2.0m - 0.50m, 
            Car => 2.00m - 1.0m,
            
            Taxi {Fares: 0} => 3.50m + 1.00m, 
            Taxi {Fares: 1} => 3.50m,
            Taxi {Fares: 2} => 3.50m - 0.50m,
            Taxi => 3.50m - 1.00m,
            
            Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, 
            Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
            Bus => 5.00m,
            
            DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, 
            DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m, 
            DeliveryTruck => 10.00m,
            
            { }             => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
            null            => throw new ArgumentNullException(nameof(vehicle))
        };
    }
  1. when 的用法也是简洁明了。当并等于某一个值,而是一个判断语句的时候用 when。
  2. 以上的代码有部分比较重复。比如对于 car 和 taxi,每个乘客数量都要写一整行代码。可以被简化为以下代码:
public decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
        Car c => c.Passengers switch
        {
            0 => 2.00m + 0.5m,
            1 => 2.0m,
            2 => 2.0m - 0.5m,
            _ => 2.00m - 1.0m
        },

        Taxi t => t.Fares switch
        {
            0 => 3.50m + 1.00m,
            1 => 3.50m,
            2 => 3.50m - 0.50m,
            _ => 3.50m - 1.00m
        },

        Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
        Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
        Bus b => 5.00m,

        DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
        DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
        DeliveryTruck t => 10.00m,

        { }  => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
        null => throw new ArgumentNullException(nameof(vehicle))
    };
  1. 可以看到根本没有新的语法。而是再写一个 switch expression。
  2. _ 表示匹配其他所有情况。同理也不能写在前面,因为一定会被匹配上。

2.3 根据高峰时间收费

假设有这么一个需求。周末正常收费。工作日的话,早上的入流量和晚上的出流量双倍收费。其他时间 1.5 倍收费。凌晨则减少为 0.75。

如果写成 if 语句,写倒是可以写,但是效果如下:

public decimal PeakTimePremiumIfElse(DateTime timeOfToll, bool inbound)
{
    if ((timeOfToll.DayOfWeek == DayOfWeek.Saturday) ||
        (timeOfToll.DayOfWeek == DayOfWeek.Sunday))
    {
        return 1.0m;
    }
    else
    {
        int hour = timeOfToll.Hour;
        if (hour < 6)
        {
            return 0.75m;
        }
        else if (hour < 10)
        {
            if (inbound)
            {
                return 2.0m;
            }
            else
            {
                return 1.0m;
            }
        }
        else if (hour < 16)
        {
            return 1.5m;
        }
        else if (hour < 20)
        {
            if (inbound)
            {
                return 1.0m;
            }
            else
            {
                return 2.0m;
            }
        }
        else // Overnight
        {
            return 0.75m;
        }
    }
}

可以用,但非常难读,也不好改。

2.3.1 使用模式匹配以及其他技巧来简化代码

仅仅使用模式匹配来匹配所有可能性也不好,依然复杂,因为我们有很多种组合情况。

2.3.1.1 周末还是工作日

第一个条件是是否为周末。那么专门为此写一个函数:

// 注意 timeOfToll.DayOfWeek 和 DayOfWeek.Monday 中的 DayOfWeek 不是一个东西。
// 前者是 DateTime 类型的一个属性,后者是一个 enum 类型。
// 前者的值也为 DayOfWeek 类型
public static bool IsWeekday(DateTime timeOfToll) =>
    timeOfToll.DayOfWeek switch {
        DayOfWeek.Monday => true,
        DayOfWeek.Tuesday => true,
        DayOfWeek.Wednesday => true,
        DayOfWeek.Thursday => true, 
        DayOfWeek.Friday => true, 
        DayOfWeek.Saturday => false,
        DayOfWeek.Sunday => false
    }

还可以再简化:

public static bool IsWeekday(DateTime timeOfToll) =>
    timeOfToll.DayOfWeek switch {
        DayOfWeek.Saturday => false,
        DayOfWeek.Sunday => false,
        _ => true
    }

2.3.1.2 一天的时间段

先看代码:

public enum TimeBand
{
    MorningRush,
    Daytime,
    EvenignRush,
    Overnight
}

public static TimeBand GetTimeBand(DateTime timeOfToll) =>
    timeOfToll.Hour switch
    {
        > 19 or < 6 => TimeBand.Overnight,
        < 10 => TimeBand.MorningRush,
        > 16 => TimeBand.EvenignRush,
        _ => TimeBand.Daytime
    };
  1. 使用了 enum 来将一天的多个时间段分配值。
  2. 使用了 > 19 or < 6 这种语法,>< 以及 or 都是在 C# 9.0 后引入的。当然还有 >=<=andnot 这些语法。(什么你问为什么没有 = 的语法,因为不需要,直接写 6 就是 =6 了)

2.3.1.3 最终代码

有了以上两个函数后,代码就可以简化为这种 tuple pattern 形式:

public static decimal CalculateToll(DateTime timeOfToll, bool isInbound) =>
    (IsWeekday(timeOfToll), GetTimeBand(timeOfToll), isInbound) switch
    {
        (true, TimeBand.MorningRush, true) => 2.00m,
        (true, TimeBand.MorningRush, false) => 1.00m,
        (true, TimeBand.Daytime, true) => 1.50m,
        (true, TimeBand.Daytime, false) => 1.50m,
        (true, TimeBand.EveningRush, true) => 1.00m,
        (true, TimeBand.EveningRush, false) => 2.00m,
        (true, TimeBand.Overnight, true) => 0.75m,
        (true, TimeBand.Overnight, false) => 0.75m,
        (false, TimeBand.MorningRush, true) => 1.00m,
        (false, TimeBand.MorningRush, false) => 1.00m,
        (false, TimeBand.Daytime, true) => 1.00m,
        (false, TimeBand.Daytime, false) => 1.00m,
        (false, TimeBand.EveningRush, true) => 1.00m,
        (false, TimeBand.EveningRush, false) => 1.00m,
        (false, TimeBand.Overnight, true) => 1.00m,
        (false, TimeBand.Overnight, false) => 1.00m,
    };

当然,很多条件可以简化:

public static decimal CalculateToll(DateTime timeOfToll, bool isInbound) =>
    (IsWeekday(timeOfToll), GetTimeBand(timeOfToll), isInbound) switch
    {
        (true, TimeBand.MorningRush, true) => 2.00m,
        (true, TimeBand.MorningRush, false) => 1.00m,
        (true, TimeBand.Daytime, _) => 1.50m,
        (true, TimeBand.EveningRush, true) => 1.00m,
        (true, TimeBand.EveningRush, false) => 2.00m,
        (true, TimeBand.Overnight, _) => 0.75m,
        (false, _, _) => 1.00m,
    };

然后可以把 3 个返回 1.00m 的用 _ 代替:

public static decimal CalculateToll(DateTime timeOfToll, bool isInbound) =>
    (IsWeekday(timeOfToll), GetTimeBand(timeOfToll), isInbound) switch
    {
        (true, TimeBand.MorningRush, true) => 2.00m,
        (true, TimeBand.Daytime, _) => 1.50m,
        (true, TimeBand.EveningRush, false) => 2.00m,
        (true, TimeBand.Overnight, _) => 0.75m,
        _ => 1.00m,
    };