简体   繁体   English

在 C# 中创建百分比类型

[英]Creating a percentage type in C#

My application deals with percentages a lot.我的应用程序经常处理百分比。 These are generally stored in the database in their written form rather than decimal form (50% would be stored as 50 rather than 0.5).这些通常以书面形式而不是十进制形式存储在数据库中(50% 将存储为 50 而不是 0.5)。 There is also the requirement that percentages are formatted consistently throughout the application.还要求百分比在整个应用程序中的格式一致。

To this end i have been considering creating a struct called percentage that encapsulates this behaviour.为此,我一直在考虑创建一个名为 percent 的结构来封装这种行为。 I guess its signature would look something like this:我猜它的签名看起来像这样:

public struct Percentage
{
    public static Percentage FromWrittenValue();
    public static Percentage FromDecimalValue();

    public decimal WrittenValue { get; set; }
    public decimal DecimalValue { get; set; }
}

Is this a reasonable thing to do?这是合理的做法吗? It would certianly encapsulate some logic that is repeated many times but it is straightforward logic that peopel are likely to understand.它肯定会封装一些重复多次的逻辑,但这是人们可能理解的直截了当的逻辑。 I guess i need to make this type behave like a normal number as much as possible however i am wary of creating implicit conversions to a from decimal in case these confuse people further.我想我需要让这种类型的行为尽可能地像一个正常的数字,但是我对创建从十​​进制到 a 的隐式转换持谨慎态度,以防这些进一步混淆人们。

Any suggestions of how to implement this class?关于如何实现这个类的任何建议? or compelling reasons not to.或令人信服的理由不这样做。

I am actually a little bit flabbergasted at the cavalier attitude toward data quality here. 实际上,我对这里对数据质量的傲慢态度感到有点惊讶。 Unfortunately, the colloquial term "percentage" can mean one of two different things: a probability and a variance. 不幸的是,口语术语“百分比”可以表示两种不同的事物之一:概率和方差。 The OP doesn't specify which, but since variance is usually calculated, I'm guessing he may mean percentage as a probability or fraction (such as a discount). OP没有指定哪个,但由于通常计算方差,我猜他可能将百分比表示为概率或分数(例如折扣)。

The extremely good reason for writing a Percentage class for this purpose has nothing to do with presentation, but with making sure that you prevent those silly silly users from doing things like entering invalid values like -5 and 250. 为此目的编写Percentage类的极好理由与表示无关,但确保您可以防止那些愚蠢的傻用户执行诸如输入-5和250之类的无效值之类的操作。

I'm thinking really more about a Probability class: a numeric type whose valid range is strictly [0,1]. 我正在考虑更多关于Probability类的问题:一种有效范围严格为[0,1]的数字类型。 You can encapsulate that rule in ONE place, rather than writing code like this in 37 places: 您可以将该规则封装在一个地方,而不是在37个地方编写这样的代码:

 public double VeryImportantLibraryMethodNumber37(double consumerProvidedGarbage)
 {
    if (consumerProvidedGarbage < 0 || consumerProvidedGarbage > 1)
      throw new ArgumentOutOfRangeException("Here we go again.");

    return someOtherNumber * consumerProvidedGarbage;
 }

instead you have this nice implementation. 相反,你有这个很好的实现。 No, it's not fantastically obvious improvement, but remember, you're doing that value-checking in each time you're using this value. 不,这并不是非常明显的改进,但请记住,每次使用此值时,您都在进行价值检查。

 public double VeryImportantLibraryMethodNumber37(Percentage guaranteedCleanData)
 {
    return someOtherNumber * guaranteedCleanData.Value;
 }

Percentage class should not be concerned with formatting itself for the UI. Percentage类不应该关注UI的格式化本身。 Rather, implement IFormatProvider and ICustomFormatter to handle formatting logic. 相反,实现IFormatProviderICustomFormatter来处理格式化逻辑。

As for conversion, I'd go with standard TypeConverter route, which would allow .NET to handle this class correctly, plus a separate PercentageParser utility class, which would delegate calls to TypeDescriptor to be more usable in external code. 至于转换,我会使用标准的TypeConverter路由,这将允许.NET正确处理此类,以及一个单独的PercentageParser实用程序类,它将委托对TypeDescriptor调用在外部代码中更加可用。 In addition, you can provide implicit or explicit conversion operator, if this is required. 此外,如果需要,您可以提供implicitexplicit转换运算符。

And when it comes to Percentage , I don't see any compelling reason to wrap simple decimal into a separate struct other than for semantic expressiveness. 当谈到Percentage ,我没有看到任何令人信服的理由将简单的decimal包装到一个单独的struct而不是语义表达。

I strongly recommend you just stick with using the double type here (I don't see any use for the decimal type either, as wouldn't actually seem to require base-10 precision in the low decimal places). 我强烈建议你坚持使用double类型(我没有看到任何使用decimal类型,因为实际上似乎不需要在低位小数位置的基数为10的精度)。 By creating a Percentage type here, you're really performing unnecessary encapsulation and just making it harder to work with the values in code. 通过在此处创建Percentage类型,您实际上正在执行不必要的封装,并且更难以使用代码中的值。 If you use a double , which is customary for storying percentages (among many other tasks), you'll find dealing with the BCL and other code a lot nicer in most cases. 如果你使用double (这是故事百分比(在许多其他任务中)的习惯),你会发现在大多数情况下处理BCL和其他代码要好得多。

The only extra functionality that I can see you need for percentages is the ability to convert to/from a percentage string easily. 我能看到的百分比所需的唯一额外功能是能够轻松转换为/从百分比字符串转换。 This can be done very simply anyway using single lines of code, or even extension methods if you want to abstract it slightly. 无论如何,使用单行代码甚至扩展方法都可以非常简单地完成,如果你想稍微抽象它。

Converting to percentage string : 转换为百分比字符串:

public static string ToPercentageString(this double value)
{
    return value.ToString("#0.0%"); // e.g. 76.2%
}

Converting from percentage string : 从百分比字符串转换:

public static double FromPercentageString(this string value)
{
    return double.Parse(value.SubString(0, value.Length - 1)) / 100;
}

It seems like a reasonable thing to do, but I'd reconsider your interface to make it more like other CLR primitive types, eg something like. 这似乎是一件合理的事情,但我会重新考虑你的界面,使其更像其他CLR原始类型,例如。

// all error checking omitted here; you would want range checks etc.
public struct Percentage
{
    public Percentage(decimal value) : this()
    {
        this.Value = value
    }

    public decimal Value { get; private set; }

    public static explicit operator Percentage(decimal d)
    {
        return new Percentage(d);
    }

    public static implicit operator decimal(Percentage p)
    {
        return this.Value;
    }

    public static Percentage Parse(string value)
    {
        return new Percentage(decimal.Parse(value));
    }

    public override string ToString()
    {
        return string.Format("{0}%", this.Value);
    }
}

You'd definitely also want to implement IComparable<T> and IEquatable<T> as well as all the corresponding operators and overrides of Equals , GetHashCode , etc. You'd also probably also want to consider implementing the IConvertible and IFormattable interfaces. 您当然也希望实现IComparable<T>IEquatable<T>以及EqualsGetHashCode等所有相应的运算符和覆盖。您可能还需要考虑实现IConvertibleIFormattable接口。

This is a lot of work. 这是很多工作。 The struct is likely to be somewhere in the region of 1000 lines and take a couple of days to do (I know this because it's a similar task to a Money struct I wrote a few months back). 该结构可能在1000行的某个地方并且需要花费几天的时间(我知道这是因为它与我几个月前写的Money结构类似)。 If this is of cost-benefit to you, then go for it. 如果这对您有成本效益,那就去吧。

这个问题让我想起了企业应用程序架构中的Money类模式 -这个链接可能会给你一些思考。

I think you may be mixing up presentation and logic here. 我想你可能在这里混淆演示和逻辑。 I would convert the percentage to a decimal or float fraction (0.5) when getting it from the database and then let the presentation deal with the formatting. 当从数据库获取它时,我会将百分比转换为小数或浮点数(0.5),然后让演示文稿处理格式。

I'd not create a separate class for that - this just creates more overhead. 我不会为此创建一个单独的类 - 这只会产生更多的开销。 I thinkg it will be faster just to use double variables set to the database value. 我认为只使用设置为数据库值的double变量会更快。

If it is common knowledge that the database stores percentages as 50 instead of 0.5, everybody will understand statemens like part = (percentage / 100.0) * (double)value . 如果众所周知数据库将百分比存储为50而不是0.5,那么每个人都会理解statements,例如part = (percentage / 100.0) * (double)value

Even in 2022, .Net 6 I found myself using something just like this.即使在 2022 年,.Net 6 我发现自己也在使用类似的东西。 I concur with Michael on his answer for the OP and like to extend it for future Googlers.我同意 Michael 对 OP 的回答,并希望将其扩展到未来的 Google 员工。

Creating a value type would be indispensable in explaining the domain's intent with enforced immutability.创建一个值类型对于解释具有强制不变性的域的意图是必不可少的。 Notice especially in the Fraction Record you will get a Quotient that would normally cause an exception however here we can safely show d / 0 with no error, likewise all other inherited children are also granted that protection (It also offers an excellent place to establish simple routines to check validity, data rehydration (as if DBA's don't make mistakes), serialization concerns just to name a few.)特别注意在分数记录中,您将获得一个通常会导致异常的商,但是在这里我们可以安全地显示 d / 0 而没有错误,同样所有其他继承的孩子也被授予保护(它还提供了一个很好的地方来建立简单的检查有效性的例程,数据再水化(好像 DBA 不会犯错误),序列化问题仅举几例。)

namespace StackOverflowing;

// Honor the simple fraction
public record class Fraction(decimal Dividend, decimal Divisor) 
{
    public decimal Quotient => (Divisor > 0.0M) ? Dividend / Divisor : 0.0M;

    // Display dividend / divisor as the string, not the quotient
    public override string ToString() 
    {
        return $"{Dividend} / {Divisor}";
    }
};

// Honor the decimal based interpretation of the simple fraction
public record class DecimalFraction(decimal Dividend, decimal Divisor) : Fraction(Dividend, Divisor)
{
    // Change the display of this type to the decimal form
    public override string ToString()
    {
        return Quotient.ToString();
    }
};

// Honor the decimal fraction as the basis value but offer a converted value as a percentage
public record class Percent(decimal Value) : DecimalFraction(Value, 100.00M)
{
    // Display the quotient as it represents the simple fraction in a base 10 format aka radix 10
    public override string ToString()
    {
        return Quotient.ToString("p");
    }
};

// Example of a domain value object consumed by an entity or aggregate in finance
public record class PercentagePoint(Percent Left, Percent Right) 
{ 
    public Percent Points => new(Left.Value - Right.Value);

    public override string ToString()
    {
        return $"{Points.Dividend} points";
    }
}



[TestMethod]
public void PercentScratchPad()
{
    var approximatedPiFraction = new Fraction(22, 7);
    var approximatedPiDecimal = new DecimalFraction(22, 7);
    var percent2 = new Percent(2);
    var percent212 = new Percent(212);
    var points = new PercentagePoint(new Percent(50), new Percent(40));

    TestContext.WriteLine($"Approximated Pi Fraction: {approximatedPiFraction}");
    TestContext.WriteLine($"Approximated Pi Decimal: {approximatedPiDecimal}");
    TestContext.WriteLine($"2 Percent: {percent2}");
    TestContext.WriteLine($"212 Percent: {percent212}");
    TestContext.WriteLine($"Percentage Points: {points}");
    TestContext.WriteLine($"Percentage Points as percentage: {points.Points}");
}

 PercentScratchPad Standard Output: TestContext Messages: PercentScratchPad 标准输出:TestContext 消息:

Approximated Pi Fraction: 22 / 7
Approximated Pi Decimal: 3.1428571428571428571428571429
2 Percent: 2.00%
212 Percent: 212.00%
Percentage Points: 10 points
Percentage Points as percentage: 10.00%

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM