🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

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 (nie DateTime.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.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak COIG, Kamsoft czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

Dodaj komentarz

czytanie to początek

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ę →