30

首先,让我解释一下目前的情况:我正在从数据库中读取记录并将它们放入一个对象中以供以后使用;今天出现了一个关于数据库类型到 C# 类型转换(强制转换?)的问题。

让我们看一个例子:

namespace Test
{
    using System;
    using System.Data;
    using System.Data.SqlClient;

    public enum MyEnum
    {
        FirstValue = 1,
        SecondValue = 2
    }

    public class MyObject
    {
        private String field_a;
        private Byte field_b;
        private MyEnum field_c;

        public MyObject(Int32 object_id)
        {
            using (SqlConnection connection = new SqlConnection("connection_string"))
            {
                connection.Open();

                using (SqlCommand command = connection.CreateCommand())
                {
                    command.CommandText = "sql_query";

                    using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow))
                    {
                        reader.Read();

                        this.field_a = reader["field_a"];
                        this.field_b = reader["field_b"];
                        this.field_c = reader["field_c"];
                    }
                }
            }
        }
    }
}

这(显然)失败了,因为这三个this.field_x = reader["field_x"];调用引发了Cannot implicitly convert type 'object' to 'xxx'. An explicit conversion exists (are you missing a cast?).编译器错误。

为了纠正这个问题,我目前知道两种方法(让我们使用这个field_b例子):第一个是this.field_b = (Byte) reader["field_b"];,第二个是this.field_b = Convert.ToByte(reader["field_b"]);

选项一的问题是DBNull字段在强制转换失败时抛出异常(即使可空类型为 as String),而二号的问题是它没有保留空值(Convert.ToString(DBNull)产生 a String.Empty),我不能使用他们也有枚举。

因此,在互联网上和 StackOverflow 上进行了几次查找之后,我想出的是:

public static class Utilities
{
    public static T FromDatabase<T>(Object value) where T: IConvertible
    {
        if (typeof(T).IsEnum == false)
        {
            if (value == null || Convert.IsDBNull(value) == true)
            {
                return default(T);
            }
            else
            {
                return (T) Convert.ChangeType(value, typeof(T));
            }
        }
        else
        {
            if (Enum.IsDefined(typeof(T), value) == false)
            {
                throw new ArgumentOutOfRangeException();
            }

            return (T) Enum.ToObject(typeof(T), value);
        }
    }
}

这样我应该处理每一个案件。

问题是:我错过了什么吗?我是否正在做 WOMBAT(浪费金钱、大脑和时间),因为有一种更快、更清洁的方法来做到这一点?这一切都正确吗?利润?

4

5 回答 5

40

如果字段允许空值,请不要使用常规原始类型。使用C#nullable类型as关键字.

int? field_a = reader["field_a"] as int?;
string field_b = reader["field_a"] as string;

将 a 添加?到任何不可为空的 C# 类型使其“可空”。使用as关键字将尝试将对象强制转换为指定类型。如果转换失败(就像类型为 时一样DBNull),则运算符返回null

注意:使用的另一个小好处as是它比普通铸造稍微快一点。由于它也可能有一些缺点,例如如果您尝试将其转换为错误的类型,则更难以跟踪错误,因此不应将其视为始终使用as传统转换的原因。常规铸造已经是一项相当便宜的操作。

于 2010-01-26T18:22:09.557 回答
13

你不想使用这些reader.Get*方法吗?唯一烦人的是它们采用列号,因此您必须将访问器包装在对GetOrdinal()的调用中

using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow))
{
    reader.Read();

    this.field_a = reader.GetString(reader.GetOrdinal("field_a"));
    this.field_a = reader.GetDouble(reader.GetOrdinal("field_b"));
    //etc
}
于 2010-01-26T18:21:50.190 回答
7

这就是我过去的处理方式:

    public Nullable<T> GetNullableField<T>(this SqlDataReader reader, Int32 ordinal) where T : struct
    {
        var item = reader[ordinal];

        if (item == null)
        {
            return null;
        }

        if (item == DBNull.Value)
        {
            return null;
        }

        try
        {
            return (T)item;
        }
        catch (InvalidCastException ice)
        {
            throw new InvalidCastException("Data type of Database field does not match the IndexEntry type.", ice);
        }
    }

用法:

int? myInt = reader.GetNullableField<int>(reader.GetOrdinal("myIntField"));
于 2010-01-26T18:52:11.320 回答
5

您可以制作一组扩展方法,每种数据类型一对:

    public static int? GetNullableInt32(this IDataRecord dr, string fieldName)
    {
        return GetNullableInt32(dr, dr.GetOrdinal(fieldName));
    }

    public static int? GetNullableInt32(this IDataRecord dr, int ordinal)
    {
        return dr.IsDBNull(ordinal) ? null : (int?)dr.GetInt32(ordinal);
    }

这实现起来有点乏味,但它非常有效。在 System.Data.DataSetExtensions.dll 中,Microsoft 使用方法解决了 DataSets 的相同问题,该Field<T>方法通常处理多种数据类型,并且可以将 DBNull 转换为 Nullable。

作为一个实验,我曾经为 DataReaders 实现了一个等效的方法,但我最终使用 Reflector 从 DataSetExtensions (UnboxT) 中借用一个内部类来有效地进行实际的类型转换。我不确定分发借来的类的合法性,所以我可能不应该分享代码,但自己查找很容易。

于 2010-01-26T18:37:13.180 回答
3

此处发布的通用处理代码很酷,但由于问题标题包含“有效”一词,我将发布我不太通用但(我希望)更有效的答案。

我建议您使用其他人提到的 getXXX 方法。为了处理 bebop 谈到的列号问题,我使用了一个枚举,如下所示:

enum ReaderFields { Id, Name, PhoneNumber, ... }
int id = sqlDataReader.getInt32((int)readerFields.Id)

这是一个额外的输入,但是您不需要调用 GetOrdinal 来查找每列的索引。而且,您不必担心列名,而是担心列位置。

要处理可为空的列,您需要检查 DBNull,并可能提供一个默认值:

string phoneNumber;
if (Convert.IsDBNull(sqlDataReader[(int)readerFields.PhoneNumber]) {
  phoneNumber = string.Empty;
}
else {
  phoneNumber = sqlDataReader.getString((int)readerFields.PhoneNumber);
}
于 2010-01-26T19:05:57.980 回答