Якось давніше пощастило прочитати короткий мануал по
правильному поводженню з
SqlDataReader.
Лінк зараз не згадаю, тому прийдеться кортко переказати зміст мануалу.
1. Старайтесь якнайменше використовувати ось такий вид
запису:
using (IDataReader reader = SPs.TestSelect().GetReader())
{
for
(int j = 0; reader.Read(); j++)
{
toReturn[j] = new TestItem();
toReturn[j].Age = (int)reader["Age"];
toReturn[j].Gender = reader["Gender"]
as string;
toReturn[j].ID = (int)reader["ID"];
toReturn[j].Login = reader["LoginName"]
as string;
toReturn[j].Name = reader["Name"]
as string;
}
}
2. Якщо настільки "приперло" написати код схожий на поданий
вище, то модифікуйте його таким чином:
using (IDataReader
reader = SPs.TestSelect().GetReader())
{
int
ageIndex = reader.GetOrdinal("Age");
int
genderIndex = reader.GetOrdinal("Gender");
int idIndex
= reader.GetOrdinal("ID");
int
loginIndex = reader.GetOrdinal("LoginName");
int
nameIndex = reader.GetOrdinal("Name");
for
(int j = 0; reader.Read(); j++)
{
toReturn[j] = new TestItem();
toReturn[j].Age = (int)reader[ageIndex];
toReturn[j].Gender = reader[genderIndex] as string;
toReturn[j].ID = (int)reader[idIndex];
toReturn[j].Login = reader[loginIndex] as string;
toReturn[j].Name = reader[nameIndex] as string;
}
}
3. А в ідеалі код має бути схожим на:
using (IDataReader reader = SPs.TestSelect().GetReader())
{
int
ageIndex = reader.GetOrdinal("Age");
int
genderIndex = reader.GetOrdinal("Gender");
int
idIndex = reader.GetOrdinal("ID");
int
loginIndex = reader.GetOrdinal("LoginName");
int
nameIndex = reader.GetOrdinal("Name");
for
(int j = 0; reader.Read(); j++)
{
toReturn[j] = new TestItem();
toReturn[j].Age = reader.GetInt32(ageIndex);
toReturn[j].Gender = reader.GetString(genderIndex);
toReturn[j].ID = reader.GetInt32(idIndex);
toReturn[j].Login = reader.GetString(loginIndex);
toReturn[j].Name = reader.GetString(nameIndex);
}
}
В прикладах toReturn –
це «TestItem[] toReturn» + ви якимось чином
дізнались правильний розмір масиву :) Результатом роботи методу є цей самий «TestItem[] toReturn»
Перше що кидається в очі, чому 3-й варіант кращий за перші два
– це unboxing при присвоєнні значень елементам масиву toReturn.
А чим другий кращий за перший? Як саме третій варіант
відрізняється від перших двох? Про це в мануалі не написали, тому прийшлось
розбиратись самотужки…
Звичайно, можна було б написати в саппорт майкрософта і
запитати у них, але я вирішив, що з Reflector-ом я справлюсь швидше :)
Хто до читав до цього
місця і вирішив, що дана тема йому малоцікава, може далі не читати. Так як далі
буде тільки дизасемблювання стандартних ліб…
Отож, пройдемось по питаннях по
порядку…
1. Чим другий варіант кращий за
перший?
Відкриваємо у рефлекторі клас SqlDataReader і дивимось на тексти
перегруженого оператора «[]».
public override object this[string name]
{
get
{
return this.GetValue(this.GetOrdinal(name));
}
}
та
public override object this[int i]
{
get
{
return this.GetValue(i);
}
}
В
першому лістінгу бачимо, що перед
викликом методу «GetValue» (який очевидно повертає потрібне нам значення :) ),
йде виклик «GetOrdinal»… Відмінність ніби і не значна, але здоровий глузд
підказує, що виконувати «GetOrdinal»
лишні 200-300 тис разів не варто :) Тому від цього моменту конструкція типу
reader["SomeKeyName"] перестає
для нас існувати.
2. Як
саме третій варіант відрізняється від перших двох?
Те що
там відсутнє явне приведення типів – це ми бачимо, але що заховано всередині..? Для прикладу препаруємо метод «GetInt32»
public override int GetInt32(int i)
{
this.ReadColumn(i);
return this._data
.Int32;
}
Звідси
ясно, що нічого не ясно :) Треба копати глибше! Виклик методу «ReadColumn» я опущу і перейду
до «return this._data
.Int32». Цікаво, а що ж за поле «_data»?
private SqlBuffer[] _data;
Хм…
Все одно нічого не ясно… Дивимось що таке «SqlBuffer» (весь код постити не буду, просто покажу те що нам
потрібне):
internal sealed class SqlBuffer
{
// Fields
private bool _isNull;
private object _object;
private StorageType _type;
private Storage _value;
// Methods
…
// Properties
…
internal object Value
{
get
{
if (this.IsNull)
{
return DBNull.Value;
}
switch (this._type)
{
case StorageType.Empty:
return DBNull.Value;
case StorageType.Boolean:
return this.Boolean;
case StorageType.Byte:
return this.Byte;
case StorageType.DateTime:
return this.DateTime;
case StorageType.Decimal:
return this.Decimal;
case StorageType.Double:
return this.Double;
case StorageType.Int16:
return this.Int16;
case StorageType.Int32:
return this.Int32;
case StorageType.Int64:
return this.Int64;
case StorageType.Money:
return this.Decimal;
case StorageType.Single:
return this.Single;
case StorageType.String:
return this.String;
case StorageType.SqlBinary:
return this.ByteArray;
case StorageType.SqlCachedBuffer:
return ((SqlCachedBuffer) this._object).ToString();
case StorageType.SqlGuid:
return this.Guid;
case StorageType.SqlXml:
{
SqlXml xml = (SqlXml) this._object;
return xml.Value;
}
}
return null;
}
}
internal int Int32
{
get
{
this.ThrowIfNull();
if (StorageType.Int32 == this._type)
{
return this._value._int32;
}
return (int) this.Value;
}
set
{
this._value._int32 = value;
this._type = StorageType.Int32;
this._isNull = false;
}
}
//Other properties
…
[StructLayout(LayoutKind.Explicit)]
internal struct Storage
{
// Fields
[FieldOffset(0)]
internal bool _boolean;
[FieldOffset(0)]
internal byte _byte;
[FieldOffset(0)]
internal SqlBuffer.DateTimeInfo _dateTimeInfo;
[FieldOffset(0)]
internal double _double;
[FieldOffset(0)]
internal short _int16;
[FieldOffset(0)]
internal int _int32;
[FieldOffset(0)]
internal long _int64;
[FieldOffset(0)]
internal SqlBuffer.NumericInfo _numericInfo;
[FieldOffset(0)]
internal float _single;
}
internal enum StorageType
{
Empty,
Boolean,
Byte,
DateTime,
Decimal,
Double,
Int16,
Int32,
Int64,
Money,
Single,
String,
SqlBinary,
SqlCachedBuffer,
SqlGuid,
SqlXml
}
}
З
приведеного вище коду видно, що значення потрібної нам табличної ячейки зберігається в одному елементі SqlBuffer. Для кожного типу даних SqlBuffer має окреме поле (див клас Storage). Отже, якщо кожен тип має своє поле, то ніякого приведення типів з object не відбувається!
Натомість
при виклику методу «GetValue»
(див текст пергруженого оператора «[]») ми витягуємо значення з проперті «Value», її значення проходить
boxing в object, а потім unboxing з
object
в потрібний нам тип. Про перфоменс в такому випадку говорити не приходиться…
Варто зауважити, що клас Storage зберігає не усі типи в окремих змінних... Для прикладу, Storage не має поля для зберігання String-ових значень. Отримання потрібного значення там виглядає ось так:
internal string String
{
get
{
this.ThrowIfNull();
if (StorageType.String == this._type)
{
return (string) this._object;
}
if (StorageType.SqlCachedBuffer == this._type)
{
return ((SqlCachedBuffer) this._object).ToString();
}
return (string) this.Value;
}
}
Тому, при витягуванні string-значення через GetValue()-метод ми отримуємо звязку з_object_в_string -> з_string_в_object -> знову_з_object_в_string! Повний ахтунг! І хоча реально платформа передає референси на обєкт (String - це ж все-таки референс тип), я вважаю, що 2 надлишкові операції - це 100% зайве!
Законним є питання, а як SqlBuffer дізнається який тип даних він
зберігає (і якому полю присвоїти прочитані з БД дані)? Це все відбувається на
початку передачі даних з БД, коли клас отримує метадані у вигляді потоку
байтів. Розписувати тут весь процес я не буду, так як це вимагає місця та часу.
P.S.: Ніякого
відкриття я звичайно ж не зробив… просто хотілось прояснити для себе які
механізми ховаються за звичайними методами. Надіюсь, що комусь це також
згодиться :)
P.P.S.: прошу вибачення за форматування коду... у мене ніяк не виходить подружитись з вбудованим редактором :(