C# 函数式风格实现 BMI 计算器

设计思路

将整个程序的业务逻辑分成以下几类:

  • 核心逻辑:只考虑正确的数据,对数据进行变换,这里是纯函数的部分
  • 异常处理:以异常来代表所有“不想理”的情况
    • 非正确数据:对于数据进行范围判断,如果处于非法范围则抛出异常
    • 其他异常:直接抛出即可
  • 输入输出
    • stdin 获取输入,转换到目标数据类型
    • 从核心逻辑获取输出,输出到 stdout
  • 基础设施 / 辅助类:需要辅助类将上述几个部分串起来,想到 javascript 中的 Promise ,它可以通过 .then 链式的连接多个流水线上的数据处理函数,并且在最后可以衔接一个 .catch 以统一处理异常,这种模式非常适合此设计需求。

核心逻辑

  • weightheight 计算出 BMI
    • 设计出 float CalcBMI(float weight, float height)
    • 考虑到需要在 Promise 中穿起来,都设计成 Func<T, R> 签名的函数更合适
    • 因此改为 float CalcBMI((float weight, float height) arg)
  • BMI 得到对应的评价文本
    • 设计出 string BMIToText(float BMI)
static float CalcBMI((float weight, float height) arg) {
    return arg.weight / (arg.height * arg.height);
}

static string BMIToText(float BMI) {
    return $"BMI {BMI:F1} is " + (
        BMI < 18.5f
        ? "underweight"
        : BMI < 25f
        ? "normal weight"
        : BMI < 30f
        ? "overweight"
        : "obesity"
    );
}

// 只读属性 Getter
static Func<(float weight, float height), string> GetBMIText
    => (arg) => BMIToText(CalcBMI(arg));

异常处理

  • 需要对 weightheight 作基本的范围限制
    • 设计出 bool IsBMIArgValid((float weight, float height) arg)
    • 以及异常版 void RaiseIfBMIArgInvalid((float weight, float height) arg)
  • 最后需要一个统一的异常处理入口
    • 考虑到我们的程序很简单,遇到异常就提示并终止即可
    • 因此这个统一的入口内部并不需要对异常进行分类诊断、作不同处理
    • 设计出 void HandleException(Exception ex)
static bool IsBMIArgValid((float weight, float height) arg) {
    return arg.weight > 0f
        && arg.height > 0f
        && arg.weight <= 200f
        && arg.height <= 2f;
}

static void RaiseIfBMIArgInvalid((float weight, float height) arg) {
    if (!IsBMIArgValid(arg)) {
        throw new ArgumentException(
            $"weight({arg.weight} or height({arg.height}) is invalid. "
            + "Valid rules: 0 < weight <= 200, 0 < height <= 2."
        );
    }
}

static void HandleException(Exception ex) {
    Console.WriteLine("Exception caught: " + ex.Message);
    Console.WriteLine("Stack Trace:\n" + ex.StackTrace);
}

输入输出

  • stdin 获取输入,转换到需求的参数类型,获取或转换失败时抛出异常
    • 设计出 (float weight, float height) GetUserInput()
  • 将 BMI 文本输出到控制台
    • 使用现成的 Console.WriteLine
  • 在程序结束时等待用户按下任意键以退出
    • 设计出 void WaitForExit()
 static (float weight, floatheight) GetUserInput() {
     Console.Write("Enter weight and height separated by space: ");
     string? input = Console.ReadLine();

     if (string.IsNullOrWhiteSpace(input)) {
         throw new ArgumentException("Input cannot be empty");
     }

     string[] parts = input.Trim().Split(' ');

     if (parts.Length != 2) {
         throw new ArgumentException(
             "Input must contain two values separated by space"
         );
     }

     float[] floats = parts.Select(float.Parse).ToArray();
     return (floats[0], floats[1]);
 }


 static void WaitForExit() {
     Console.WriteLine("Press any key to exit...");
     Console.ReadKey();
 }

基础设施 / 辅助类

  • 需要实现一个 Promise 来将业务函数串起来
    • 需要 Promise<R> Then<R>(Func<T, R> onFulfilled) 来连接核心逻辑
    • 需要 static Promise<T> Begin(Func<T> produce) 来开始数据处理链
    • 需要 Promise<T> End(Action<T> consume) 来终止数据处理链
    • 需要 void Catch(Action<Exception> onRejected) 来统一处理过程中的异常
public class Promise<T> {
    private T? Value { get; }
    private Exception? Ex { get; }

    private Promise(T? value, Exception? ex) {
        if (value == null && ex == null) {
            throw new ArgumentException(
                "Either value or exception must be non-null"
            );
        }
        Value = value;
        Ex = ex;
    }

    public static Promise<T> Resolve(T value) {
        return new Promise<T>(value, null);
    }

    public static Promise<T> Reject(Exception ex) {
        return new Promise<T>(default, ex);
    }

    public static Promise<T> Begin(Func<T> produce) {
        return Promise<T>
           .Resolve(default)  // 任何参数无所谓,反正不消耗
           .Then((_) => produce());
    }

    public Promise<T> End(Action<T> consume) {
        return Then(
            (value) => { consume(value); return value; }
        );
    }

    public Promise<R> Then<R>(Func<T, R> onFulfilled) {
        if (Ex != null || Value == null) {
            return new Promise<R>(default, Ex);
        }
        try {
            return new Promise<R>(onFulfilled(Value), null);
        }
        catch (Exception ex) {
            return new Promise<R>(default, ex);
        }
    }

    public void Catch(Action<Exception> onRejected) {
        if (Value != null || Ex == null) {
            return;
        }
        onRejected(Ex);
    }
}

意识到 RaiseIfBMIArgInvalid 函数会消费掉数据而不继续传递,因此需要

  • 设计装饰器函数将 Action<T> 包装为 Func<T, T>
    • 设计出 Func<T, T> PassWithAssert<T>(Action<T> assert)
public class FuncTools {
    public static Func<T, T> PassWithAssert<T>(Action<T> assert) {
        return (value) => {
            assert(value);
            return value;
        };
    }
}

优化

注意到 Promise 有 2 处实现有问题:

  • Begin 方法的 .Resolve(default) 提示 default 可能为 null
    • 这不是可能,而是一定,但直接写 null 又不行
  • End 方法的 consume(value); return value; 会冗余的返回 value,这可能被意外使用

解决方案是设计一个 NoneType 类,它只有一个唯一的值 None
None 将表示这个值从不被消费也没有任何价值,它仅作为一个类型或值的占位符存在

public class NoneType {
    public static readonly NoneType None = new();

    private NoneType() { }  // 私有构造函数,确保只能有一个 None 实例

    public override string ToString() {
        return "None";
    }
}

完整实现

Program.cs

using static bmi.FuncTools;

namespace bmi {
    public class Program {
        // 主函数
        static void Main(string[] _) {
            var passWithAssert = PassWithAssert<(float weight, float height)>
                (RaiseIfBMIArgInvalid);

            Promise<(float weight, float height)>
                .Begin(GetUserInput)
                .Then(passWithAssert)
                .Then(GetBMIText)
                .End(Console.WriteLine)
                .Catch(HandleException);

            WaitForExit();
        }

        // - 核心业务逻辑

        static float CalcBMI((float weight, float height) arg) {
            return arg.weight / (arg.height * arg.height);
        }

        static string BMIToText(float BMI) {
            return $"BMI {BMI:F1} is " + (
                BMI < 18.5f
                ? "underweight"
                : BMI < 25f
                ? "normal weight"
                : BMI < 30f
                ? "overweight"
                : "obesity"
            );
        }

        static Func<(float weight, float height), string> GetBMIText
            => (arg) => BMIToText(CalcBMI(arg));

        // - 异常处理逻辑

        static bool IsBMIArgValid((float weight, float height) arg) {
            return arg.weight > 0f
                && arg.height > 0f
                && arg.weight <= 200f
                && arg.height <= 2f;
        }

        static void RaiseIfBMIArgInvalid((float weight, float height) arg) {
            if (!IsBMIArgValid(arg)) {
                throw new ArgumentException(
                    $"weight({arg.weight} or height({arg.height}) is invalid. "
                    + "Valid rules: 0 < weight <= 200, 0 < height <= 2."
                );
            }
        }

        static void HandleException(Exception ex) {
            Console.WriteLine("Exception caught: " + ex.Message);
            Console.WriteLine("Stack Trace:\n" + ex.StackTrace);
        }

        // - 输入输出逻辑

        static (float weight, float height) GetUserInput() {
            Console.Write("Enter weight and height separated by space: ");
            string? input = Console.ReadLine();

            if (string.IsNullOrWhiteSpace(input)) {
                throw new ArgumentException("Input cannot be empty");
            }

            string[] parts = input.Trim().Split(' ');

            if (parts.Length != 2) {
                throw new ArgumentException(
                    "Input must contain two values separated by space"
                );
            }

            float[] floats = parts.Select(float.Parse).ToArray();
            return (floats[0], floats[1]);
        }


        static void WaitForExit() {
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }
}

FuncTools.cs

namespace bmi {
    public class FuncTools {
        public static Func<T, T> PassWithAssert<T>(Action<T> assert) {
            return (value) => {
                assert(value);
                return value;
            };
        }
    }
}

Promise.cs

namespace bmi {
    public class Promise<T> {
        private T? Value { get; }
        private Exception? Ex { get; }

        private Promise(T? value, Exception? ex) {
            if (value == null && ex == null) {
                throw new ArgumentException(
                    "Either value or exception must be non-null"
                );
            }
            Value = value;
            Ex = ex;
        }

        public static Promise<T> Resolve(T value) {
            return new Promise<T>(value, null);
        }

        public static Promise<T> Reject(Exception ex) {
            return new Promise<T>(default, ex);
        }

        public static Promise<T> Begin(Func<T> produce) {
            return Promise<NoneType>
               .Resolve(NoneType.None)  // 任何参数无所谓,反正不消耗
               .Then((_) => produce());
        }

        public Promise<NoneType> End(Action<T> consume) {
            return Then(
                (value) => { consume(value); return NoneType.None; }
            );
        }

        public Promise<R> Then<R>(Func<T, R> onFulfilled) {
            if (Ex != null || Value == null) {
                return new Promise<R>(default, Ex);
            }
            try {
                return new Promise<R>(onFulfilled(Value), null);
            }
            catch (Exception ex) {
                return new Promise<R>(default, ex);
            }
        }

        public void Catch(Action<Exception> onRejected) {
            if (Value != null || Ex == null) {
                return;
            }
            onRejected(Ex);
        }
    }
}

NoneType.cs

namespace bmi {
    public class NoneType {
        public static readonly NoneType None = new();

        private NoneType() { }  // 私有构造函数,确保只能有一个 None 实例

        public override string ToString() {
            return "None";
        }
    }
}
  • 15
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个基于委托实现的简单计算器示例,包含加、减、乘、除四种运算: ```csharp using System; namespace Calculator { class Program { delegate double Calculate(double x, double y); static void Main(string[] args) { Console.WriteLine("请输入第一个数:"); double num1 = double.Parse(Console.ReadLine()); Console.WriteLine("请输入第二个数:"); double num2 = double.Parse(Console.ReadLine()); Console.WriteLine("请选择运算符(+、-、*、/):"); string op = Console.ReadLine(); Calculate calc = null; switch (op) { case "+": calc = new Calculate(Add); break; case "-": calc = new Calculate(Subtract); break; case "*": calc = new Calculate(Multiply); break; case "/": calc = new Calculate(Divide); break; default: Console.WriteLine("非法运算符!"); break; } if (calc != null) { double result = calc(num1, num2); Console.WriteLine("计算结果为:" + result); } Console.ReadLine(); } static double Add(double x, double y) { return x + y; } static double Subtract(double x, double y) { return x - y; } static double Multiply(double x, double y) { return x * y; } static double Divide(double x, double y) { if (y == 0) { Console.WriteLine("除数不能为0!"); return 0; } else { return x / y; } } } } ``` 该示例中,定义了一个名为 `Calculate` 的委托类型,它接受两个 `double` 参数并返回一个 `double` 值。在 `Main` 方法中,通过用户输入的运算符选择不同的计算方法,并将其赋值给 `Calculate` 委托类型的变量。最后,通过调用委托变量可以执行相应的计算方法,并输出结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值