DBNull을 확인한 다음 변수에 할당하는 가장 효율적인 방법은 무엇입니까?


151

이 질문은 때때로 나타나지만 만족스러운 답변을 보지 못했습니다.

일반적인 패턴은 다음과 같습니다 (행은 DataRow입니다 ).

 if (row["value"] != DBNull.Value)
 {
      someObject.Member = row["value"];
 }

첫 번째 질문은 더 효율적인 것입니다 (조건을 뒤집 었습니다).

  row["value"] == DBNull.Value; // Or
  row["value"] is DBNull; // Or
  row["value"].GetType() == typeof(DBNull) // Or... any suggestions?

이것은 .GetType ()이 더 빠르다는 것을 나타내지 만 컴파일러는 내가 모르는 몇 가지 트릭을 알고 있습니까?

두 번째 질문은 row [ "value"]의 값을 캐싱 할 가치가 있습니까, 아니면 컴파일러가 인덱서를 최적화합니까?

예를 들면 다음과 같습니다.

  object valueHolder;
  if (DBNull.Value == (valueHolder = row["value"])) {}

노트:

  1. 행 [ "값"]이 존재합니다.
  2. 열의 열 인덱스를 알지 못하므로 열 이름 조회입니다.
  3. DBNull을 확인한 다음 할당 (조기 최적화 등이 아닌)에 대해 구체적으로 묻습니다.

몇 가지 시나리오 (초, 10,000,000 회 시도)를 벤치마킹했습니다.

row["value"] == DBNull.Value: 00:00:01.5478995
row["value"] is DBNull: 00:00:01.6306578
row["value"].GetType() == typeof(DBNull): 00:00:02.0138757

Object.ReferenceEquals는 "=="와 동일한 성능을 갖습니다.

가장 흥미로운 결과? 대소 문자를 기준으로 열 이름이 일치하지 않으면 (예 : "value"대신 "Value") 문자열의 길이가 약 10 배 길어집니다.

row["Value"] == DBNull.Value: 00:00:12.2792374

이야기의 교훈은 인덱스로 열을 찾을 수 없다면 인덱서에 공급하는 열 이름이 DataColumn의 이름과 정확히 일치하는지 확인하는 것입니다.

값 캐싱도 거의 두 배 빠릅니다.

No Caching: 00:00:03.0996622
With Caching: 00:00:01.5659920

따라서 가장 효율적인 방법 은 다음과 같습니다 .

 object temp;
 string variable;
 if (DBNull.Value != (temp = row["value"]))
 {
      variable = temp.ToString();
 }

1
행이 DataRow인지 아니면 IDataRecord / IDataReader인지 확인할 수 있습니까?
Marc Gravell

7
이제 훨씬 더 나은 .NET Framework가 있으며 DataRowExtensions Methods를 사용할 수 있습니다 .
Pavel Hodek

대소 문자를 기준으로 열 이름이 일치하지 않으면 (예 : "value"대신 "Value") 약 10 배 더 오래 걸립니다 (문자열의 경우) . 구현에 따라 완전히 달라집니다. MySQL ADO.NET 커넥터의 경우 열 이름이 훨씬 느리지 만 SqlServer 또는 SQLite의 경우에는 전혀 기억 나지 않습니다 (기억하지 마십시오). 지금 상황이 바뀌었을 수도 있습니다.
nawfal

@PavelHodek DataRow에만 해당되는 수치입니다. IDataRecord확장 기능 을 좋아했을 것 입니다.
nawfal

답변:


72

뭔가 빠졌을 것입니다. 방법이 DBNull정확히 무엇인지 확인 DataRow.IsNull하지 않습니까?

다음 두 가지 확장 방법을 사용했습니다.

public static T? GetValue<T>(this DataRow row, string columnName) where T : struct
{
    if (row.IsNull(columnName))
        return null;

    return row[columnName] as T?;
}

public static string GetText(this DataRow row, string columnName)
{
    if (row.IsNull(columnName))
        return string.Empty;

    return row[columnName] as string ?? string.Empty;
}

용법:

int? id = row.GetValue<int>("Id");
string name = row.GetText("Name");
double? price = row.GetValue<double>("Price");

Nullable<T>대한 반환 값을 원하지 않으면 GetValue<T>쉽게 반환 default(T)하거나 다른 옵션을 사용할 수 있습니다 .


관련이없는 메모에 Stevo3000의 제안에 대한 VB.NET 대안이 있습니다.

oSomeObject.IntMember = If(TryConvert(Of Integer)(oRow("Value")), iDefault)
oSomeObject.StringMember = If(TryCast(oRow("Name"), String), sDefault)

Function TryConvert(Of T As Structure)(ByVal obj As Object) As T?
    If TypeOf obj Is T Then
        Return New T?(DirectCast(obj, T))
    Else
        Return Nothing
    End If
End Function

3
Dan은 OP가 피하고 싶은 것을 다시 위험에 빠뜨립니다. 글 row.IsNull(columnName)을 쓰면 이미 한 번 읽고 다시 읽습니다. 그것이 변화를 가져올 것이라고 말하지는 않지만 이론적으로는 효율성이 떨어질 수 있습니다.
nawfal

2
System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)본질적으로 첫 번째 방법과 동일한 작업을 수행 하지 않습니까?
Dennis G

35

다음 방법을 사용해야합니다.

Convert.IsDBNull()

프레임 워크에 내장되어 있다는 것을 고려할 때 이것이 가장 효율적이라고 기대합니다.

나는 다음과 같은 내용을 제안한다.

int? myValue = (Convert.IsDBNull(row["column"]) ? null : (int?) Convert.ToInt32(row["column"]));

그리고 네, 컴파일러는 당신을 위해 그것을 캐시해야합니다.


5
음, 모든 언급 한 옵션은 프레임 워크에 내장되어 있습니다 ... 사실, Convert.IsDBNull는 ... IConvertible에 관한 추가 작업을 많이 수행
마크 Gravell

1
조건부 예제를 의미하는 경우 캐시를 다시 가져 와서는 안됩니다. 실제로해서는 안됩니다. 인덱서를 두 번 실행합니다.
Marc Gravell

아, 그리고 그 코드는 컴파일되지 않지만 그중 하나에 (int?)를 추가하면 (IL에서) 2의 다음을 볼 수 있습니다 : callvirt instance object [System.Data] System.Data.DataRow :: get_Item (string)
Marc Gravell

20

컴파일러는 인덱서를 최적화하지 않습니다 (예 : row [ "value"]를 두 번 사용하는 경우). 예, 약간 더 빠릅니다.

object value = row["value"];

그런 다음 값을 두 번 사용하십시오. .GetType ()을 사용하면 null 인 경우 문제가 발생할 수 있습니다.

DBNull.Value실제로는 싱글 톤이므로 4 번째 옵션을 추가합니다. 아마도 ReferenceEquals를 사용할 수 있습니다. 그러나 실제로는, 당신이 여기서 너무 걱정하고 있다고 생각합니다 ... "is", "== "등은 현재보고있는 성능 문제의 원인이 될 것입니다. 전체 코드를 프로파일 링 하고 중요한 것에 집중하십시오. 그렇지 않을 것입니다.


2
사실상 모든 경우 ==는 ReferenceEquals (예 : DBNull)와 동일하며 훨씬 더 읽기 쉽습니다. 원하는 경우 @Marc Gravell의 최적화를 사용하십시오.하지만 그와 함께합니다. 아마 도움이되지 않을 것입니다. BTW, 참조 평등은 항상 유형 검사를 능가해야합니다.
tvanfosson

1
지금은 오래되었지만 최근에 이것이 프로파일 러가 해결했다고 정확히 말한 많은 사례를 보았습니다. 모든 셀이이 검사를 수행해야하는 대규모 데이터 세트를 평가한다고 상상해보십시오. 이를 최적화하면 큰 보상을 얻을 수 있습니다. 그러나 대답의 중요한 부분은 여전히 ​​유용합니다. 먼저 가장 좋은 시간을 알기 위해 먼저 프로파일 하십시오.
Joel Coehoorn '

Elvis 연산자를 C # 6에 도입하면 제안한 검사에서 null 참조 예외를 쉽게 피할 수 있다고 생각합니다. value? .GetType () == typeof (DBNull)
Eniola

그래, 난 동의. 일반적으로 더 나은 방법이지만 위험이 지적한 .GetType ()을 사용하지 않으려는 사람들에게는 적합합니다. 그 주위에 방법을 제공합니다.
Eniola

9

C #에서 다음 코드를 사용합니다 ( VB.NET 은 간단하지 않습니다).

이 코드는 null / DBNull이 아닌 경우 값을 할당하고, 그렇지 않으면 컴파일러에서 할당을 무시할 수 있도록 LHS 값으로 설정할 수있는 기본값을 할당합니다.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;

1
VB.NET 버전 간단 oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault)합니다.
Dan Tao

1
@ Dan Tao-그 코드를 컴파일했다고 생각하지 않습니다. 코드가 작동하지 않는 이유를 설명하는 오래된 질문을 살펴보십시오. stackoverflow.com/questions/746767/…
stevehipwell

그리고 다시 한 번, 내 컴퓨터에서 (도구가있는 도구) 떨어져있는 동안 SO 질문에 대해 언급하는 것은 실수로 판명되었습니다! 당신이 맞아요. TryCastC #의 as연산자 Nullable(Of T)유형 과 동일한 편리한 기능을 제공하지 않는다는 사실에 놀랐습니다 . 내가 이것을 모방한다고 생각할 수있는 가장 가까운 방법은 지금 내 대답에서 제안한 것처럼 자신의 함수를 작성하는 것입니다.
Dan Tao

이것을 일반적인 방법으로 리팩토링하는 데 어려움을 겪을 것이며, 그렇게해도 캐스팅이 너무 많으면 효율이 떨어집니다.
nawfal

8

나는 여기에 접근하는 방법이 거의 없다고 생각하지만 OP가 가장 걱정하는 부분 (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand)은 대부분 복잡하지 않습니다. 이것이 쓸모없는 마이크로 최적화라는 것을 완전히 알고 있으므로 기본적으로 다음을 사용해야한다고 말하겠습니다.

1) DataReader / DataRow에서 값을 두 번 읽지 마십시오. 따라서 null 확인 및 캐스트 / 변환 전에 값을 캐시하거나 record[X]적절한 서명을 사용 하여 객체를 사용자 정의 확장 메소드에 직접 전달하는 것이 좋습니다 .

2) 위의 내용을 준수하려면 내부적으로 IsDBNull호출하기 때문에 DataReader / DataRow에 내장 함수를 사용하지 마십시오 record[X]. 실제로 두 번 수행합니다.

3) 형식 비교는 일반적으로 값 비교보다 항상 느립니다. 그냥 할 record[X] == DBNull.Value더 나은.

4) 직접 캐스팅은 Convert변환 클래스를 호출하는 것보다 빠를 것이지만 후자는 덜 흔들릴 것입니다.

5) 마지막으로 열 이름이 아닌 인덱스로 레코드에 액세스하는 것이 다시 빠릅니다.


나는 Szalay, Neil 및 Darren Koppand의 접근 방식이 더 좋을 것이라고 생각합니다. 나는 특히 Darren Koppand의 확장 방법 접근 방식을 선호하며 IDataRecord(더 좁히고 싶지만 IDataReader) 색인 / 열 이름을 사용합니다.

주의해서 호출하십시오.

record.GetColumnValue<int?>("field");

그리고 아닙니다

record.GetColumnValue<int>("field");

경우에 당신은 구별해야 0하고 DBNull. 예를 들어 열거 형 필드에 null 값이 있으면 default(MyEnum)첫 번째 열거 형 값이 반환 될 위험이 있습니다. 더 나은 전화 record.GetColumnValue<MyEnum?>("Field").

당신이에서 읽고 있기 때문에 DataRow, 나는 모두 확장 메서드를 만들 것입니다 DataRowIDataReader의해 건조 공통 코드입니다.

public static T Get<T>(this DataRow dr, int index, T defaultValue = default(T))
{
    return dr[index].Get<T>(defaultValue);
}

static T Get<T>(this object obj, T defaultValue) //Private method on object.. just to use internally.
{
    if (obj.IsNull())
        return defaultValue;

    return (T)obj;
}

public static bool IsNull<T>(this T obj) where T : class 
{
    return (object)obj == null || obj == DBNull.Value;
} 

public static T Get<T>(this IDataReader dr, int index, T defaultValue = default(T))
{
    return dr[index].Get<T>(defaultValue);
}

이제 다음과 같이 호출하십시오.

record.Get<int>(1); //if DBNull should be treated as 0
record.Get<int?>(1); //if DBNull should be treated as null
record.Get<int>(1, -1); //if DBNull should be treated as a custom value, say -1

나는 이것이 런타임 예외가없고 null 값을 유연하게 처리 할 수 ​​있도록 프레임 워크 (예 record.GetInt32: record.GetString등 대신)에 있어야하는 방법이라고 생각 합니다.

내 경험상 데이터베이스에서 읽을 수있는 일반적인 방법이 적었습니다. 난 내 자신 작성했다, 그래서 나는 항상, 사용자 정의 핸들 다양한 형태했다 GetInt, GetEnum, GetGuid장기적 등의 방법을. 기본적으로 db에서 문자열을 읽을 때 공백을 자르거나 DBNull빈 문자열로 취급하려면 어떻게해야합니까? 또는 소수점 이하의 모든 0을자를 경우. Guid기본 데이터베이스가 문자열 또는 이진으로 저장할 수있는 경우 다른 커넥터 드라이버가 다르게 동작하는 유형에 가장 어려움이있었습니다 . 다음과 같은 과부하가 있습니다.

static T Get<T>(this object obj, T defaultValue, Func<object, T> converter)
{
    if (obj.IsNull())
        return defaultValue;

    return converter  == null ? (T)obj : converter(obj);
}

Stevo3000의 접근 방식을 사용하면 호출이 약간 추악하고 지루한 것으로 나타 났으며 일반적인 기능을 수행하기가 더 어려울 것입니다.


7

객체가 문자열 일 수있는 번거로운 경우가 있습니다. 아래의 확장 메소드 코드는 모든 경우를 처리합니다. 사용 방법은 다음과 같습니다.

    static void Main(string[] args)
    {
        object number = DBNull.Value;

        int newNumber = number.SafeDBNull<int>();

        Console.WriteLine(newNumber);
    }



    public static T SafeDBNull<T>(this object value, T defaultValue) 
    {
        if (value == null)
            return default(T);

        if (value is string)
            return (T) Convert.ChangeType(value, typeof(T));

        return (value == DBNull.Value) ? defaultValue : (T)value;
    } 

    public static T SafeDBNull<T>(this object value) 
    { 
        return value.SafeDBNull(default(T)); 
    } 

6

필자는이 구문을 선호합니다.이 구문은에 의해 노출 된 명시 적 IsDbNull 메서드를 사용하고 IDataRecord중복 문자열 조회를 피하기 위해 열 인덱스를 캐시합니다.

가독성을 높이기 위해 다음과 같이 진행됩니다.

int columnIndex = row.GetOrdinal("Foo");
string foo; // the variable we're assigning based on the column value.
if (row.IsDBNull(columnIndex)) {
  foo = String.Empty; // or whatever
} else { 
  foo = row.GetString(columnIndex);
}

DAL 코드의 압축을 위해 한 줄에 맞게 다시 작성했습니다.이 예에서는 int bar = -1if row["Bar"]가 null 인 경우를 지정 합니다 .

int i; // can be reused for every field.
string foo  = (row.IsDBNull(i  = row.GetOrdinal("Foo")) ? null : row.GetString(i));
int bar = (row.IsDbNull(i = row.GetOrdinal("Bar")) ? -1 : row.GetInt32(i));

인라인 할당은 알지 못하는 경우 혼란 스럽지만 전체 작업을 한 줄로 유지하므로 한 코드 블록의 여러 열에서 속성을 채울 때 가독성이 향상됩니다.


3
DataRow는 IDataRecord를 구현하지 않습니다.
ilitirit

5

내가 이것을하지는 않았지만 이중 인덱서 호출을 피하고 정적 / 확장 메소드를 사용하여 코드를 깨끗하게 유지할 수 있습니다.

즉.

public static IsDBNull<T>(this object value, T default)
{
    return (value == DBNull.Value)
        ? default
        : (T)value;
}

public static IsDBNull<T>(this object value)
{
    return value.IsDBNull(default(T));
}

그때:

IDataRecord record; // Comes from somewhere

entity.StringProperty = record["StringProperty"].IsDBNull<string>(null);
entity.Int32Property = record["Int32Property"].IsDBNull<int>(50);

entity.NoDefaultString = record["NoDefaultString"].IsDBNull<string>();
entity.NoDefaultInt = record["NoDefaultInt"].IsDBNull<int>();

또한 널 검사 로직을 한 곳에 유지하는 이점이 있습니다. 물론 단점은 추가 메소드 호출이라는 것입니다.

그냥 생각이야


2
그러나 객체에 확장 메소드를 추가하는 것은 매우 광범위합니다. 개인적으로 DataRow에서 확장 방법을 고려했지만 객체는 아닙니다.
Marc Gravell

그러나 확장 메서드는 확장 클래스의 네임 스페이스를 가져올 때만 사용할 수 있습니다.
Richard Szalay 2016

5

가능한 한이 점검을 피하려고합니다.

보유 할 수없는 열에 대해서는 반드시 수행 할 필요는 없습니다 null.

Nullable 값 형식 ( int?등)으로 저장하는 경우을 사용하여 변환 할 수 있습니다 as int?.

당신은 구별 할 필요가없는 경우 string.Emptynull, 당신은 단지 호출 할 수 있습니다 .ToString()DBNull이 반환하기 때문에, string.Empty.


4

나는 항상 사용합니다 :

if (row["value"] != DBNull.Value)
  someObject.Member = row["value"];

짧고 포괄적이라는 것을 알았습니다.


4

이것이 DataRows에서 읽기를 처리하는 방법입니다.

///<summary>
/// Handles operations for Enumerations
///</summary>
public static class DataRowUserExtensions
{
    /// <summary>
    /// Gets the specified data row.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="dataRow">The data row.</param>
    /// <param name="key">The key.</param>
    /// <returns></returns>
    public static T Get<T>(this DataRow dataRow, string key)
    {
        return (T) ChangeTypeTo<T>(dataRow[key]);
    }

    private static object ChangeTypeTo<T>(this object value)
    {
        Type underlyingType = typeof (T);
        if (underlyingType == null)
            throw new ArgumentNullException("value");

        if (underlyingType.IsGenericType && underlyingType.GetGenericTypeDefinition().Equals(typeof (Nullable<>)))
        {
            if (value == null)
                return null;
            var converter = new NullableConverter(underlyingType);
            underlyingType = converter.UnderlyingType;
        }

        // Try changing to Guid  
        if (underlyingType == typeof (Guid))
        {
            try
            {
                return new Guid(value.ToString());
            }
            catch

            {
                return null;
            }
        }
        return Convert.ChangeType(value, underlyingType);
    }
}

사용 예 :

if (dbRow.Get<int>("Type") == 1)
{
    newNode = new TreeViewNode
                  {
                      ToolTip = dbRow.Get<string>("Name"),
                      Text = (dbRow.Get<string>("Name").Length > 25 ? dbRow.Get<string>("Name").Substring(0, 25) + "..." : dbRow.Get<string>("Name")),
                      ImageUrl = "file.gif",
                      ID = dbRow.Get<string>("ReportPath"),
                      Value = dbRow.Get<string>("ReportDescription").Replace("'", "\'"),
                      NavigateUrl = ("?ReportType=" + dbRow.Get<string>("ReportPath"))
                  };
}

몬스터에 대한 소품 ChageTypeTo 코드로 My .Net 을 얻었습니다.


4

확장 방법과 비슷한 것을했습니다. 내 코드는 다음과 같습니다.

public static class DataExtensions
{
    /// <summary>
    /// Gets the value.
    /// </summary>
    /// <typeparam name="T">The type of the data stored in the record</typeparam>
    /// <param name="record">The record.</param>
    /// <param name="columnName">Name of the column.</param>
    /// <returns></returns>
    public static T GetColumnValue<T>(this IDataRecord record, string columnName)
    {
        return GetColumnValue<T>(record, columnName, default(T));
    }

    /// <summary>
    /// Gets the value.
    /// </summary>
    /// <typeparam name="T">The type of the data stored in the record</typeparam>
    /// <param name="record">The record.</param>
    /// <param name="columnName">Name of the column.</param>
    /// <param name="defaultValue">The value to return if the column contains a <value>DBNull.Value</value> value.</param>
    /// <returns></returns>
    public static T GetColumnValue<T>(this IDataRecord record, string columnName, T defaultValue)
    {
        object value = record[columnName];
        if (value == null || value == DBNull.Value)
        {
            return defaultValue;
        }
        else
        {
            return (T)value;
        }
    }
}

그것을 사용하려면 다음과 같이하십시오.

int number = record.GetColumnValue<int>("Number",0)

4

DataRow에서 행 [ "fieldname"] isDbNull을 0으로 바꾸면 그렇지 않으면 10 진수 값을 얻습니다.

decimal result = rw["fieldname"] as decimal? ?? 0;

3
public static class DBH
{
    /// <summary>
    /// Return default(T) if supplied with DBNull.Value
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <returns></returns>
    public static T Get<T>(object value)
    {   
        return value == DBNull.Value ? default(T) : (T)value;
    }
}

이런 식으로 사용

DBH.Get<String>(itemRow["MyField"])

3

데이터베이스에서 많은 데이터를 읽는 프로그램에 IsDBNull이 있습니다. IsDBNull을 사용하면 약 20 초 안에 데이터를로드합니다. IsDBNull이 없으면 약 1 초입니다.

그래서 나는 그것을 사용하는 것이 낫다고 생각합니다.

public String TryGetString(SqlDataReader sqlReader, int row)
{
    String res = "";
    try
    {
        res = sqlReader.GetString(row);
    }
    catch (Exception)
    { 
    }
    return res;
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.