欢迎光临散文网 会员登陆 & 注册

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

2021-11-28 16:26 作者:oyishyi  | 我要投稿

好了,忙的时候结束了。

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

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

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

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,    };


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

分享到微博请遵守国家法律