TLDR:
Wiele odpowiedzi z twierdzeniami na temat wydajności i złych praktyk, więc wyjaśniam to tutaj.
Trasa wyjątku jest szybsza dla większej liczby zwracanych kolumn, trasa pętli jest szybsza dla mniejszej liczby kolumn, a punkt podziału wynosi około 11 kolumn. Przewiń w dół, aby zobaczyć wykres i kod testowy.
Pełna odpowiedź:
Kod niektórych najważniejszych odpowiedzi działa, ale tutaj jest debata na temat „lepszej” odpowiedzi opartej na akceptacji obsługi wyjątków w logice i związanej z nią wydajności.
Aby to wyjaśnić, nie wierzę, że istnieje wiele wskazówek dotyczących wyjątków ŁOWIENIE. Microsoft ma pewne wskazówki dotyczące wyjątków WYPROWADZANIE. Tam stwierdzają:
Jeśli to możliwe, NIE używaj wyjątków dla normalnego przepływu kontroli.
Pierwsza uwaga to łagodność „jeśli to możliwe”. Co ważniejsze, opis podaje następujący kontekst:
framework designers should design APIs so users can write code that does not throw exceptions
Oznacza to, że jeśli piszesz interfejs API, który może być używany przez kogoś innego, daj mu możliwość poruszania się po wyjątku bez próbowania / chwytania. Na przykład podaj TryParse ze swoją metodą analizy wyjątków Parse. Nigdzie nie mówi to jednak, że nie powinieneś wychwytywać wyjątku.
Ponadto, jak zauważa inny użytkownik, połowy zawsze pozwalały na filtrowanie według typu, a nieco później na dalsze filtrowanie poprzez klauzulę when . Wydaje się to stratą funkcji językowych, jeśli nie powinniśmy ich używać.
Można powiedzieć, że istnieje NIEKTÓRY koszt rzuconego wyjątku, który może MNIE wpłynąć na wydajność w ciężkiej pętli. Można jednak powiedzieć, że koszt wyjątku będzie znikomy w „połączonej aplikacji”. Rzeczywisty koszt zbadano ponad dziesięć lat temu: https://stackoverflow.com/a/891230/852208
Innymi słowy, koszt połączenia i zapytania do bazy danych może być niższy niż w przypadku zgłoszonego wyjątku.
Poza tym chciałem ustalić, która metoda naprawdę jest szybsza. Zgodnie z oczekiwaniami nie ma konkretnej odpowiedzi.
Każdy kod, który zapętla się nad kolumnami, staje się wolniejszy w miarę istnienia liczby kolumn. Można również powiedzieć, że każdy kod, który opiera się na wyjątkach, zwolni w zależności od częstotliwości, w której zapytanie nie zostanie znalezione.
Biorąc odpowiedzi zarówno Chada Granta, jak i Matta Hamiltona, zastosowałem obie metody z maksymalnie 20 kolumnami i do 50% poziomu błędu (OP wskazał, że używa tego testu między różnymi procesami, więc założyłem, że tylko dwa) .
Oto wyniki przedstawione za pomocą LinqPad:
Zygzaki tutaj są wskaźnikami błędów (nie znaleziono kolumny) w obrębie każdej liczby kolumn.
W porównaniu z węższymi zestawami wyników, zapętlenie jest dobrym wyborem. Jednak metoda GetOrdinal / Exception nie jest tak wrażliwa na liczbę kolumn i zaczyna przewyższać metodę zapętlania około 11 kolumn.
To powiedziawszy, tak naprawdę nie mam preferencji pod względem wydajności, ponieważ 11 kolumn brzmi rozsądnie, ponieważ średnia liczba kolumn zwrócona w całej aplikacji. W obu przypadkach mówimy tutaj o ułamkach milisekundy.
Jednak z punktu widzenia prostoty kodu i obsługi aliasów prawdopodobnie wybrałbym trasę GetOrdinal.
Oto test w formie linqpad. Zapraszam do repost z własną metodą:
void Main()
{
var loopResults = new List<Results>();
var exceptionResults = new List<Results>();
var totalRuns = 10000;
for (var colCount = 1; colCount < 20; colCount++)
{
using (var conn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=master;Integrated Security=True;"))
{
conn.Open();
//create a dummy table where we can control the total columns
var columns = String.Join(",",
(new int[colCount]).Select((item, i) => $"'{i}' as col{i}")
);
var sql = $"select {columns} into #dummyTable";
var cmd = new SqlCommand(sql,conn);
cmd.ExecuteNonQuery();
var cmd2 = new SqlCommand("select * from #dummyTable", conn);
var reader = cmd2.ExecuteReader();
reader.Read();
Func<Func<IDataRecord, String, Boolean>, List<Results>> test = funcToTest =>
{
var results = new List<Results>();
Random r = new Random();
for (var faultRate = 0.1; faultRate <= 0.5; faultRate += 0.1)
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var faultCount=0;
for (var testRun = 0; testRun < totalRuns; testRun++)
{
if (r.NextDouble() <= faultRate)
{
faultCount++;
if(funcToTest(reader, "colDNE"))
throw new ApplicationException("Should have thrown false");
}
else
{
for (var col = 0; col < colCount; col++)
{
if(!funcToTest(reader, $"col{col}"))
throw new ApplicationException("Should have thrown true");
}
}
}
stopwatch.Stop();
results.Add(new UserQuery.Results{
ColumnCount = colCount,
TargetNotFoundRate = faultRate,
NotFoundRate = faultCount * 1.0f / totalRuns,
TotalTime=stopwatch.Elapsed
});
}
return results;
};
loopResults.AddRange(test(HasColumnLoop));
exceptionResults.AddRange(test(HasColumnException));
}
}
"Loop".Dump();
loopResults.Dump();
"Exception".Dump();
exceptionResults.Dump();
var combinedResults = loopResults.Join(exceptionResults,l => l.ResultKey, e=> e.ResultKey, (l, e) => new{ResultKey = l.ResultKey, LoopResult=l.TotalTime, ExceptionResult=e.TotalTime});
combinedResults.Dump();
combinedResults
.Chart(r => r.ResultKey, r => r.LoopResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
.AddYSeries(r => r.ExceptionResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
.Dump();
}
public static bool HasColumnLoop(IDataRecord dr, string columnName)
{
for (int i = 0; i < dr.FieldCount; i++)
{
if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase))
return true;
}
return false;
}
public static bool HasColumnException(IDataRecord r, string columnName)
{
try
{
return r.GetOrdinal(columnName) >= 0;
}
catch (IndexOutOfRangeException)
{
return false;
}
}
public class Results
{
public double NotFoundRate { get; set; }
public double TargetNotFoundRate { get; set; }
public int ColumnCount { get; set; }
public double ResultKey {get => ColumnCount + TargetNotFoundRate;}
public TimeSpan TotalTime { get; set; }
}