C#中针对Int32的扩展方法

7

我设想有能力编写流畅的代码,为代码库中的数字添加含义。比如你想把一个数字表示为英里距离,你可能会这样写:

用法:

var result = myMethod(100.Miles());

我认为这比简单传递int更易读,并且您可以对Miles类型应用边界检查。
扩展方法和结构实现:
static class IntExtensions
{
  public static Miles(this int i) { get { return new Miles { Count = i }; } }
}

public struct Miles
{ 
  public int Count { get; private set; } //optionally perform bounds checking
} 

这个想法有用吗?还是在炎热的星期五太晚了?

编辑:是的,没有扩展属性看起来不太整洁...抱歉,代码有误匆忙提交的。这只是一个想法。


3
你的扩展方法和类定义是错误的。 - this. __curious_geek
1
你如何设想使用它?为所有类型的单位添加东西吗?100.英里() * 2.43.千克() / 454.月球周期()? - simendsjo
你的意思是“envision”而不是“envisage”,虽然在你的单一实现中可能很好,但在我的金融应用程序中我不需要它。我想要“.Dollars”和“.Euros”。 - StingyJack
@simendsjo:我做了这个,一开始我并不确定这是否是一个好主意,但最终我真的很喜欢它的实现和使用模式,至少对于我在其中使用的系统而言。如果你想进一步探索,我在下面举了一个例子。 - codekaizen
1
顺便提一下,这个概念已经被整合到 F# 中了:http://blogs.msdn.com/b/andrewkennedy/archive/2008/08/20/units-of-measure-in-f-part-one-introducing-units.aspx - Dan Bryant
9个回答

5
这是一个有趣的想法,但在执行此类操作之前,我建议先确定一个非常强大的使用案例。首先,一旦将数字转换为“英里”,您就不能再将其视为整数。您必须实现整个运算符范围,或在对它们进行算术运算之前将英里转换回整数。如果没有充分的理由,这会增加很多额外工作。
尽管如此,有些情况下这将是一个好策略。NASA曾因程序员向函数传递错误的度量单位而损失了1.25亿美元的太空船,这种方法可以帮助您避免这种问题。
另外,您可能会对F#感兴趣,它具有内置的度量单位支持

4

在编程中,你不应该使用魔法数字,这是有很好的理由的。编写扩展方法并不能有效地解决这个问题。你仍然需要处理一个漂浮的魔法数字。

如果它是常量,请将其定义为常量,并在常量的名称中包含_ MILES _。

另外,为什么不将该值封装在一个名为Distance的类或结构中,该类或结构只包含数字值和指定计量单位的枚举值?

例如:

public class Distance {
    private double _distanceValue;
    private UnitOfMeasure _uom;

    public double DistanceValue {
        get { return _distanceValue; }
        set { _distanceValue = value; }
    }

        public UnitOfMeasure Uom {
        get { return _uom; }
        set { _uom = value; }
    }
}

public enum UnitOfMeasure {
    Kilometers,
    Miles,
    Feet,
    Inches,
    Parsecs
}

1
虽然我不喜欢魔数,但我认为有一种情况是可以使用魔数的,那就是在策略模式中使用此模式创建策略时。在我使用它的领域中,能够快速在代码中创建新策略并将其插入到流程框架中非常重要。由于策略体现了领域逻辑,因此将实现策略所需的值放入代码中是很自然的。这种方法使编写和阅读非常容易。 - codekaizen
这对我来说读起来很好: var result = myMethod(new Distance(100, UnitOfMeasure.Miles)); 此外,想象一下如果您的“Distance”类有一个“ConvertTo(UnitOfMeasure)”方法,更好的是,它是由一个基类指定并被“Mass”、“Weight”等类继承。 - Paul Sasik
1
为什么不将值包装在一个名为“Distance”的类或结构体中,因为这样你就失去了任何编译时检查。 - Konrad Rudolph
@Konrad - 这是正确的,也是我发现将它们区分为不同类型的最大好处之一,即使我必须动态处理这些类型(例如dbReader.GetDouble(0).Celsius()),而不是静态处理(例如21.3.Celsius())。 - codekaizen

2

你的Miles结构体应该是不可变的。

将其更改为

public struct Miles { 
    public Miles(int count) : this() { Count = count; } //optionally perform bounds checking

    public int Count { get; private set; } 
} 

2

一条评论:将 Miles 设为可变的有什么意义?int 是不可变的,为什么在有单位后要使它可变?

(此外,在 C# 4 中是否引入了扩展属性?否则这将无法工作。)

最后,如果您想要添加单位,它们应该是可组合的,而我目前不知道如何实现这一点。

例如,以下代码应该编译:

var x = 100.km;
var y = 10.sec;
var kmh = x / y; // What type does kmh have?

C++中有一个库通过将类型表示为七个基本物理单位的维度元组来实现此功能,但是在C#中这种方法不起作用,因为它需要整数作为模板参数。


2

我使用这个想法来创建一个处理物理量的项目的内部语法。一开始我对这种方法持怀疑态度,但现在我真的很喜欢它,因为它使源代码非常易读、易写,而且有趣。以下是一个示例:

一种单位类型:

public struct Celsius : IEquatable<Celsius>
{
    private readonly Double _value;
    public const string Abbreviation = "°C";

    public Celsius(Double value)
    {
        _value = value;
    }

    public Boolean Equals(Celsius other)
    {
        return _value == other._value;
    }

    public override Boolean Equals(Object other)
    {
        if (!(other is Celsius))
        {
            return false;
        }

        return Equals((Celsius)other);
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        return _value + Abbreviation;
    }

    public static explicit operator Celsius(Double value)
    {
        return new Celsius(value);
    }

    public static explicit operator Double(Celsius value)
    {
        return value._value;
    }

    public static Boolean operator >(Celsius l, Celsius r)
    {
        return l._value > r._value;
    }

    public static bool operator <(Celsius l, Celsius r)
    {
        return l._value < r._value;
    }

    public static Boolean operator >=(Celsius l, Celsius r)
    {
        return l._value >= r._value;
    }

    public static bool operator <=(Celsius l, Celsius r)
    {
        return l._value <= r._value;
    }

    public static Boolean operator ==(Celsius l, Celsius r)
    {
        return l._value == r._value;
    }

    public static bool operator !=(Celsius l, Celsius r)
    {
        return l._value != r._value;
    }   
}

单元扩展类:

public static class UnitsExtensions
{

    public static Celsius Celsius(this Double value)
    {
        return new Celsius(value);
    }

    public static Celsius Celsius(this Single value)
    {
        return new Celsius(value);
    }

    public static Celsius Celsius(this Int32 value)
    {
        return new Celsius(value);
    }

    public static Celsius Celsius(this Decimal value)
    {
        return new Celsius((Double)value);
    }

    public static Celsius? Celsius(this Decimal? value)
    {
        return value == null ? default(Celsius?) : new Celsius((Double)value);
    }
}

用法:

var temp = (Celsius)value;

if (temp <= 0.Celsius())
{
    Console.Writeline("It's cold!");
}
else if (temp < 20.Celsius())
{
    Console.Writeline("Chilly...");
}
else if (temp < 30.Celsius())
{
    Console.Writeline("It's quite lovely");
}
else
{
    Console.Writeline("It's hot!");
}

我有许多这些类型的变量,例如毫米弧度度数每秒毫米等。我甚至实现了除法,以便在将每秒毫米除以毫米时,我会得到一个时间段值作为返回值。也许这有点过头了,但我发现使用这些类型能够带来类型安全和思维上的轻松,因此实现和维护它们是值得的。


4
你肯定不住在挪威 :) 0意味着“天气相当美好”,20意味着“很热!”,30意味着“简直无法忍受!”,大于30意味着“别开玩笑了!” - simendsjo
2
我知道在处理相同“单位”时,这非常有用;10.摄氏度() - 10.华氏度(),但如果能够混合使用单位,例如10.千米() / 2.秒()并得到5千米/秒就更好了。 - simendsjo
1
@simendsjo - 我用 MillimetersPerSecond / Millimeters => TimeSpan 做了这件事。它运行得相当不错。如果我能够为 TimeSpan 分配一个“扩展运算符”,允许 Millimeters/TimeSpan = MillimetersPerSecond,那么它将运行得更好。 - codekaizen

1

这是你的设计应该如何。

请注意,C#中还没有扩展属性,只有扩展方法。

class Program
{
    static void Main(string[] args)
    {
        var result = myMethod(100.ToMiles());
        //Miles miles = 100.ToMiles();
    }        
}

static class IntExtensions
{
    public static Miles ToMiles(this int miles)
    {
        return new Miles(miles);
    }
}

struct Miles
{
    public int Count { get; private set; }

    public Miles(int count)
        : this()
    {
        if (count < 0)
        {
            throw new ArgumentException("miles type cannot hold negative values.");
        }
        this.Count = count;
    }
}

1
为什么要冗余前缀 To100.Miles()已经足够清晰明了。 - Konrad Rudolph
1
该方法应指定一个能够传达其目的的名称。ToMiles() 传达了该方法将 int 类型转换为英里的信息。就像任何 .Net 类型一样,int.ToString() 方法将 int 值转换为其字符串表示形式。 - this. __curious_geek
1
我同意 .ToMiles() 的概念。它符合其他方法,比如 ToString() 或者 Convert 的各种方法。 - Anthony Pegram
1
如果您可以假设起始值始终是固定的度量单位(例如公里),那么ToMiles函数才能正常工作。如果它可以是任意距离,则ToMiles将没有意义。 - Paul Sasik
我在“To”前缀上有些不同意,原因有两个:它使代码更难读取,并且与数字到数字的转换不同,因为从字符串返回是一种解析操作而不是转换操作。 - codekaizen
显示剩余4条评论

1

我从之前的SO问题中(稍作修改)获取了这个代码。我非常喜欢这种风格,因为它符合像DateTime和TimeSpan这样的常见方法。

[StructLayout(LayoutKind.Sequential), ComVisible(true)]
    public struct Distance : IEquatable<Distance>, IComparable<Distance>
    {
        private const double MetersPerKilometer = 1000.0;
        private const double CentimetersPerMeter = 100.0;
        private const double CentimetersPerInch = 2.54;
        private const double InchesPerFoot = 12.0;
        private const double FeetPerYard = 3.0;
        private const double FeetPerMile = 5280.0;
        private const double FeetPerMeter = CentimetersPerMeter / (CentimetersPerInch * InchesPerFoot);
        private const double InchesPerMeter = CentimetersPerMeter / CentimetersPerInch;

        public static readonly Distance Zero = new Distance(0.0);

        private readonly double meters;

        /// <summary>
        /// Initializes a new Distance to the specified number of meters.
        /// </summary>
        /// <param name="meters"></param>
        public Distance(double meters)
        {
            this.meters = meters;
        }

        /// <summary>
        /// Gets the value of the current Distance structure expressed in whole and fractional kilometers. 
        /// </summary>
        public double TotalKilometers
        {
            get
            {
                return meters / MetersPerKilometer;
            }
        }

        /// <summary>
        /// Gets the value of the current Distance structure expressed in whole and fractional meters. 
        /// </summary>
        public double TotalMeters
        {
            get
            {
                return meters;
            }
        }

        /// <summary>
        /// Gets the value of the current Distance structure expressed in whole and fractional centimeters. 
        /// </summary>
        public double TotalCentimeters
        {
            get
            {
                return meters * CentimetersPerMeter;
            }
        }

        /// <summary>
        /// Gets the value of the current Distance structure expressed in whole and fractional yards. 
        /// </summary>
        public double TotalYards
        {
            get
            {
                return meters * FeetPerMeter / FeetPerYard;
            }
        }

        /// <summary>
        /// Gets the value of the current Distance structure expressed in whole and fractional feet. 
        /// </summary>
        public double TotalFeet
        {
            get
            {
                return meters * FeetPerMeter;
            }
        }

        /// <summary>
        /// Gets the value of the current Distance structure expressed in whole and fractional inches. 
        /// </summary>
        public double TotalInches
        {
            get
            {
                return meters * InchesPerMeter;
            }
        }

        /// <summary>
        /// Gets the value of the current Distance structure expressed in whole and fractional miles. 
        /// </summary>
        public double TotalMiles
        {
            get
            {
                return meters * FeetPerMeter / FeetPerMile;
            }
        }

        /// <summary>
        /// Returns a Distance that represents a specified number of kilometers.
        /// </summary>
        /// <param name="value">A number of kilometers.</param>
        /// <returns></returns>
        public static Distance FromKilometers(double value)
        {
            return new Distance(value * MetersPerKilometer);
        }

        /// <summary>
        /// Returns a Distance that represents a specified number of meters.
        /// </summary>
        /// <param name="value">A number of meters.</param>
        /// <returns></returns>
        public static Distance FromMeters(double value)
        {
            return new Distance(value);
        }

        /// <summary>
        /// Returns a Distance that represents a specified number of centimeters.
        /// </summary>
        /// <param name="value">A number of centimeters.</param>
        /// <returns></returns>
        public static Distance FromCentimeters(double value)
        {
            return new Distance(value / CentimetersPerMeter);
        }

        /// <summary>
        /// Returns a Distance that represents a specified number of yards.
        /// </summary>
        /// <param name="value">A number of yards.</param>
        /// <returns></returns>
        public static Distance FromYards(double value)
        {
            return new Distance(value * FeetPerYard / FeetPerMeter);
        }

        /// <summary>
        /// Returns a Distance that represents a specified number of feet.
        /// </summary>
        /// <param name="value">A number of feet.</param>
        /// <returns></returns>
        public static Distance FromFeet(double value)
        {
            return new Distance(value / FeetPerMeter);
        }

        /// <summary>
        /// Returns a Distance that represents a specified number of inches.
        /// </summary>
        /// <param name="value">A number of inches.</param>
        /// <returns></returns>
        public static Distance FromInches(double value)
        {
            return new Distance(value / InchesPerMeter);
        }

        /// <summary>
        /// Returns a Distance that represents a specified number of miles.
        /// </summary>
        /// <param name="value">A number of miles.</param>
        /// <returns></returns>
        public static Distance FromMiles(double value)
        {
            return new Distance(value * FeetPerMile / FeetPerMeter);
        }

        public static bool operator ==(Distance a, Distance b)
        {
            return (a.meters == b.meters);
        }

        public static bool operator !=(Distance a, Distance b)
        {
            return (a.meters != b.meters);
        }

        public static bool operator >(Distance a, Distance b)
        {
            return (a.meters > b.meters);
        }

        public static bool operator >=(Distance a, Distance b)
        {
            return (a.meters >= b.meters);
        }

        public static bool operator <(Distance a, Distance b)
        {
            return (a.meters < b.meters);
        }

        public static bool operator <=(Distance a, Distance b)
        {
            return (a.meters <= b.meters);
        }

        public static Distance operator +(Distance a, Distance b)
        {
            return new Distance(a.meters + b.meters);
        }

        public static Distance operator -(Distance a, Distance b)
        {
            return new Distance(a.meters - b.meters);
        }

        public static Distance operator -(Distance a)
        {
            return new Distance(-a.meters);
        }

        public override bool Equals(object obj)
        {
            if (!(obj is Distance))
                return false;

            return Equals((Distance)obj);
        }

        public bool Equals(Distance value)
        {
            return this.meters == value.meters;
        }

        public int CompareTo(Distance value)
        {
            return this.meters.CompareTo(value.meters);
        }

        public override int GetHashCode()
        {
            return meters.GetHashCode();
        }

        public override string ToString()
        {
            return string.Format("{0} meters", TotalMeters);
        }
    }

0

就我个人而言,我没有看到任何意义。

我认为myMethod的签名没有理由不应该是:

public object MyMethod(int miles)
{
    // bounds checking on int here
    // then logic 
}

你也可以使用代码合同来使事情更加明确。

添加一个对.Miles()的调用并将int Mutable变量化会更加令人困惑。


2
你也可以使用代码合同来使事情更加明确。但是,使用类型而不是合同更加明确,并且提供更好的编译器支持。如果您将类型系统视为允许您对代码进行断言和不变量的证明系统(实际上,这就是类型系统的定义),那么添加单位就有很多意义。除了调用方站点上的代码合同需要远程复制更多代码之外。 - Konrad Rudolph
@Konrad Rudolph - 当我建议使用代码合同时,我指的是边界检查逻辑和验证。使用代码合同可以让他更好地定义预期值的范围,并允许编译器在开发人员的值可能超出范围时发出警告。我同意类型更明确,但在这种情况下,它只是Int32的简单包装器,我认为没有增加价值。 - Justin Niessner
“它只是一个简单的包装器...没有增加价值”这表明了对类型存在的误解。类型不必捆绑数据或添加大量操作。不同类型存在的事实是类型的重要目的。否则,为什么物理学中会有不同的单位?任何物理学家都会告诉你,当你进行计算(在纸上或计算机上)并且最终的单位一致时,计算很可能也是正确的。相同的论点适用于事实检查(=使用强类型编译)。考虑:否则为什么存在TimeSpan类? - Konrad Rudolph
@Konrad Rudolph - 我确实理解你的观点。但我仍然不确定在这种情况下是否同意。哦......还有 TimeSpan 对象可以添加大量有用的方法到值中,以及以几种不同的方式表示时间间隔。也许不是最好的例子。 - Justin Niessner

0
public static class Int32Extensions
{
    public static Miles ToMiles( this Int32 distance )
    {
        return new Miles( distance );
    }
}

public class Miles
{
    private Int32 _distance;

    public Miles( Int32 distance )
    {
        _distance = distance;
    }

    public Int32 Distance
    {
        get
        {
            return _distance;
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接