Klikacz klawiszy — Zadanie rekrutacyjne #16

Czego szuka rekruter:
Stopwatch, pomiar czasu, event loop, prezentacja wyników
Poziom: Junior
Zadanie
Napisz program, który mierzy czas między kolejnymi naciśnięciami klawisza przez użytkownika. Wyświetl czas między naciśnięciami i statystyki (min, max, średnia). Program kończy się po naciśnięciu Esc.
Czego szuka rekruter
To zadanie o pomiarze czasu i zarządzaniu stanem. Rekruter sprawdza:
- Znajomość
Stopwatch(nieDateTime.Now— to zegar ścienny, nie wysokorozdzielczy timer) - Event loop z
Console.ReadKey - Gromadzenie wyników i obliczanie statystyk
- Obsługa pierwszego naciśnięcia (nie ma poprzedniego — brak pomiaru)
Najczęstszy błąd: DateTime.Now zamiast Stopwatch. DateTime ma rozdzielczość ~10-15ms na Windows, Stopwatch ma rozdzielczość do setek nanosekund.
Dlaczego Stopwatch a nie DateTime?
// ❌ DateTime — niższa rozdzielczość, narzut systemowy
DateTime start = DateTime.Now;
// ... naciśnięcie klawisza ...
TimeSpan elapsed = DateTime.Now - start;
// ✅ Stopwatch — wysokorozdzielczy timer sprzętowy
Stopwatch sw = Stopwatch.StartNew();
// ... naciśnięcie klawisza ...
TimeSpan elapsed = sw.Elapsed;
sw.Restart();
✅ Rozwiązanie
static void KeyClickMeasurer()
{
var intervals = new List<TimeSpan>();
var sw = new Stopwatch();
bool firstPress = true;
Console.WriteLine("Klikaj klawisze. Esc = koniec.");
Console.WriteLine("─────────────────────────────");
while (true)
{
ConsoleKeyInfo key = Console.ReadKey(intercept: true); // intercept: nie wypisuj znaku
if (key.Key == ConsoleKey.Escape)
break;
if (firstPress)
{
sw.Restart();
firstPress = false;
Console.WriteLine($"[{key.KeyChar}] Pierwsze naciśnięcie — zaczynam pomiar.");
continue;
}
TimeSpan interval = sw.Elapsed;
intervals.Add(interval);
sw.Restart();
Console.WriteLine($"[{key.KeyChar}] {interval.TotalMilliseconds:F1} ms");
}
PrintStats(intervals);
}
static void PrintStats(List<TimeSpan> intervals)
{
Console.WriteLine("\n─────────────────────────────");
if (intervals.Count == 0)
{
Console.WriteLine("Brak danych do statystyk.");
return;
}
double minMs = intervals.Min(t => t.TotalMilliseconds);
double maxMs = intervals.Max(t => t.TotalMilliseconds);
double avgMs = intervals.Average(t => t.TotalMilliseconds);
double totalS = intervals.Sum(t => t.TotalSeconds);
Console.WriteLine($"Naciśnięcia: {intervals.Count}");
Console.WriteLine($"Min: {minMs:F1} ms");
Console.WriteLine($"Max: {maxMs:F1} ms");
Console.WriteLine($"Średnia: {avgMs:F1} ms");
Console.WriteLine($"Tempo: {intervals.Count / totalS:F1} klik/s");
}
Stopwatch — kluczowe metody
var sw = new Stopwatch();
sw.Start(); // uruchom
sw.Stop(); // zatrzymaj (zachowuje ElapsedTime)
sw.Reset(); // zatrzymaj i zeruj
sw.Restart(); // zeruj i uruchom (Start + Reset w jednym)
sw.Elapsed // TimeSpan
sw.ElapsedMilliseconds // long (ms)
sw.ElapsedTicks // long (ticki procesora)
sw.IsRunning // bool
// Pomiar jednorazowy:
var elapsed = Stopwatch.StartNew();
DoWork();
Console.WriteLine(elapsed.ElapsedMilliseconds);
Rozszerzone statystyki — odchylenie standardowe
static double StandardDeviation(List<TimeSpan> intervals)
{
if (intervals.Count < 2) return 0;
double avg = intervals.Average(t => t.TotalMilliseconds);
double variance = intervals.Average(t => Math.Pow(t.TotalMilliseconds - avg, 2));
return Math.Sqrt(variance);
}
Testy (logika statystyk)
[Fact]
public void PrintStats_EmptyList_DoesNotThrow()
{
// Sprawdź że pusta lista nie rzuca wyjątku
var intervals = new List<TimeSpan>();
Assert.Empty(intervals); // właściwa asercja zależy od implementacji
}
[Fact]
public void Stats_Calculation_IsCorrect()
{
var intervals = new List<TimeSpan>
{
TimeSpan.FromMilliseconds(100),
TimeSpan.FromMilliseconds(200),
TimeSpan.FromMilliseconds(300),
};
Assert.Equal(100, intervals.Min(t => t.TotalMilliseconds));
Assert.Equal(300, intervals.Max(t => t.TotalMilliseconds));
Assert.Equal(200, intervals.Average(t => t.TotalMilliseconds));
}
Co powiedzieć na rozmowie
“Stopwatch używa licznika czasu wysokiej rozdzielczości procesora (QueryPerformanceCounter na Windows) — rozdzielczość sub-milisekundowa. DateTime.Now to zegar ścienny z rozdzielczością ~15ms. Do pomiaru czasu wykonania zawsze używam Stopwatch. Console.ReadKey(intercept: true) przechwytuje klawisz bez wypisywania go — bez tego każde naciśnięcie byłoby widoczne w konsoli.”
Podsumowanie
Kluczowe: Stopwatch zamiast DateTime, sw.Restart() do pomiaru kolejnych odstępów, Console.ReadKey(intercept: true) do “cichego” przechwytywania klawiszy. Osobna metoda dla statystyk sprawia że jest testowalna.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →