检查对象是否为数字

我想检查一个对象是否是一个数字,这样 .ToString()就会产生一个包含数字和 +-.的字符串

在.NET 中通过简单的类型检查是否可行? 比如:

if (p is Number)

或者我应该转换为 string,然后尝试解析为 double

更新: 为了澄清我的对象是 intuintfloatdouble等等,它不是 string。 我试图创建一个函数,这个函数可以像下面这样将任何对象序列化为 XML:

<string>content</string>

或者

<numeric>123.3</numeric>

或者提出一个例外。

153912 次浏览

You will simply need to do a type check for each of the basic numeric types.

Here's an extension method that should do the job:

public static bool IsNumber(this object value)
{
return value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is int
|| value is uint
|| value is long
|| value is ulong
|| value is float
|| value is double
|| value is decimal;
}

This should cover all numeric types.

Update

It seems you do actually want to parse the number from a string during deserialisation. In this case, it would probably just be best to use double.TryParse.

string value = "123.3";
double num;
if (!double.TryParse(value, out num))
throw new InvalidOperationException("Value is not a number.");

Of course, this wouldn't handle very large integers/long decimals, but if that is the case you just need to add additional calls to long.TryParse / decimal.TryParse / whatever else.

Yes, this works:

object x = 1;
Assert.That(x is int);

For a floating point number you would have to test using the float type:

object x = 1f;
Assert.That(x is float);

Assuming your input is a string...

There are 2 ways:

use Double.TryParse()

double temp;
bool isNumber = Double.TryParse(input, out temp);

use Regex

 bool isNumber = Regex.IsMatch(input,@"-?\d+(\.\d+)?");

You could use code like this:

if (n is IConvertible)
return ((IConvertible) n).ToDouble(CultureInfo.CurrentCulture);
else
// Cannot be converted.

If your object is an Int32, Single, Double etc. it will perform the conversion. Also, a string implements IConvertible but if the string isn't convertible to a double then a FormatException will be thrown.

There are three different concepts there:

  • to check if it is a number (i.e. a (typically boxed) numeric value itself), check the type with is - for example if(obj is int) {...}
  • to check if a string could be parsed as a number; use TryParse()
  • but if the object isn't a number or a string, but you suspect ToString() might give something that looks like a number, then call ToString() and treat it as a string

In both the first two cases, you'll probably have to handle separately each numeric type you want to support (double/decimal/int) - each have different ranges and accuracy, for example.

You could also look at regex for a quick rough check.

Taken from Scott Hanselman's Blog:

public static bool IsNumeric(object expression)
{
if (expression == null)
return false;


double number;
return Double.TryParse( Convert.ToString( expression
, CultureInfo.InvariantCulture)
, System.Globalization.NumberStyles.Any
, NumberFormatInfo.InvariantInfo
, out number);
}

Take advantage of the IsPrimitive property to make a handy extension method:

public static bool IsNumber(this object obj)
{
if (Equals(obj, null))
{
return false;
}


Type objType = obj.GetType();
objType = Nullable.GetUnderlyingType(objType) ?? objType;


if (objType.IsPrimitive)
{
return objType != typeof(bool) &&
objType != typeof(char) &&
objType != typeof(IntPtr) &&
objType != typeof(UIntPtr);
}


return objType == typeof(decimal);
}

EDIT: Fixed as per comments. The generics were removed since .GetType() boxes value types. Also included fix for nullable values.

If your requirement is really

.ToString() would result in a string containing digits and +,-,.

and you want to use double.TryParse then you need to use the overload that takes a NumberStyles parameter, and make sure you are using the invariant culture.

For example for a number which may have a leading sign, no leading or trailing whitespace, no thousands separator and a period decimal separator, use:

NumberStyles style =
NumberStyles.AllowLeadingSign |
NumberStyles.AllowDecimalPoint |
double.TryParse(input, style, CultureInfo.InvariantCulture, out result);

There are some great answers above. Here is an all-in-one solution. Three overloads for different circumstances.

// Extension method, call for any object, eg "if (x.IsNumeric())..."
public static bool IsNumeric(this object x) { return (x==null ? false : IsNumeric(x.GetType())); }


// Method where you know the type of the object
public static bool IsNumeric(Type type) { return IsNumeric(type, Type.GetTypeCode(type)); }


// Method where you know the type and the type code of the object
public static bool IsNumeric(Type type, TypeCode typeCode) { return (typeCode == TypeCode.Decimal || (type.IsPrimitive && typeCode != TypeCode.Object && typeCode != TypeCode.Boolean && typeCode != TypeCode.Char)); }

Rather than rolling your own, the most reliable way to tell if an in-built type is numeric is probably to reference Microsoft.VisualBasic and call Information.IsNumeric(object value). The implementation handles a number of subtle cases such as char[] and HEX and OCT strings.

While writing my own object.IsNumeric() extension method based on Saul Dolgin's answer to this question I ran into a potential issue in that you will get an OverflowException if you try it with double.MaxValue or double.MinValue.

My "solution" was to combine the accepted answer from Noldorin with the one from Saul Dolgin and add a pattern matching switch before trying to parse anything (and use some C#7 goodness to tidy up a bit):

public static bool IsNumeric(this object obj)
{
if (obj == null) return false;


switch (obj)
{
case sbyte _: return true;
case byte _: return true;
case short _: return true;
case ushort _: return true;
case int _: return true;
case uint _: return true;
case long _: return true;
case ulong _: return true;
case float _: return true;
case double _: return true;
case decimal _: return true;
}


string s = Convert.ToString(obj, CultureInfo.InvariantCulture);


return double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out double _);
}

Here is an even shorter version of @Noldorin's excellent answer:

public static Boolean IsNumeric(this Object obj) =>
obj is SByte or Byte or Int16 or UInt16 or Int32 or UInt32 or Int64 or UInt64 or Single or Double or Decimal;

Don't overlook that .NET includes Microsoft.VisualBasic.Information.IsNumeric() which can be used from C# just as well as VB. This may be preferable to writing your own, or at least use it as a basis.


The way it works is to first look for certain types that can be "converted" to or parsed as numbers (strings, chars, or char arrays), and then finally just checks the type code to see if it is one of the intrinsic numeric types.

If you want to exclude strings or chars then you could use it like so:

if (foo.GetType().IsValueType && !(foo is char) && Information.IsNumeric(foo))
{
// Numeric...
}

Reference source does not seem to include this, but it can be viewed in Visual Studio:

public static bool IsNumeric(object Expression)
{
IConvertible convertible = Expression as IConvertible;
if (convertible == null)
{
char[] array = Expression as char[];
if (array == null)
{
return false;
}


Expression = new string(array);
}


TypeCode typeCode = convertible.GetTypeCode();
if (typeCode == TypeCode.String || typeCode == TypeCode.Char)
{
string value = convertible.ToString(null);
try
{
long i64Value = default(long);
if (Utils.IsHexOrOctValue(value, ref i64Value))
{
return true;
}
}
catch (StackOverflowException ex) { throw ex; }
catch (OutOfMemoryException ex2) { throw ex2; }
catch (ThreadAbortException ex3) { throw ex3; }
catch (Exception) { return false; }


double Result = default(double);
return DoubleType.TryParse(value, ref Result);
}


return IsOldNumericTypeCode(typeCode);
}

They way it checks for explicit numeric types is similar to answer https://stackoverflow.com/a/18857243/3195477 although its logic is inverted in a way which may be more robust/safe: it only returns true for a known numeric type, as opposed to returning false for known non-numeric types; the latter could lead to a bug if ever other types are added.

internal static bool IsOldNumericTypeCode(TypeCode TypCode)
{
switch (TypCode)
{
case TypeCode.Boolean:
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
return true;
default:
return false;
}
}

In .NET 7 a new API was introduced to support generic math, which offers lots of interfaces that are implemented in all numerical types. There is an interface INumber<TSelf> which can be used as type constraint for generic methods to only allow for numeric types (int, double, decimal, ...):

public void FooBar<T>(T number) where T : INumber<T>
{
// ...number is a numeric type.
}

There are also more fine-grained interfaces for categories of numeric types:

  • IBinaryInteger<T> for byte, short, int, long, ...
  • IFloatingPoint<T> for float, double, decimal, ...
  • ...

This way, you can define one or more generic type constraints specifically tailored to the numeric types that you expect to handle in your methods along with the benefits of type safety. This should be your preferred route to take and should save you from expensive or faulty checks and passing object around.


If you must rely on object, you can at least use the new INumber<T> interface to check with reflection if it is a numeric type. You would check if the type implements any concrete INumber<T> interface, by comparing their generic type definitions to the open generic INumber<> type. Please be aware that reflection may incur performance costs that must be considered.

private bool IsNumericType(object obj)
{
return obj
.GetType()
.GetInterfaces()
.Any(@interface => @interface.GetGenericTypeDefinition() == typeof(INumber<>));
}

The API might provide other useful interfaces for your purpose, like for formatting and parsing.