سؤال

لدي تطبيق وحدة تحكم أريد أن أقدمه للمستخدم س ثواني للرد على المطالبة.إذا لم يتم إجراء أي إدخال بعد فترة زمنية معينة، فيجب أن يستمر منطق البرنامج.نحن نفترض أن المهلة تعني استجابة فارغة.

ما هي الطريقة الأكثر مباشرة للتعامل مع هذا؟

هل كانت مفيدة؟

المحلول

وتفاجأت عندما علمت أنه بعد مرور 5 سنوات، لا تزال جميع الإجابات تعاني من واحدة أو أكثر من المشاكل التالية:

  • يتم استخدام وظيفة أخرى غير ReadLine، مما يتسبب في فقدان الوظيفة.(حذف/مسافة للخلف/مفتاح لأعلى للإدخال السابق).
  • تتصرف الوظيفة بشكل سيئ عند استدعائها عدة مرات (إنشاء عدة سلاسل رسائل، أو العديد من خطوط ReadLine المعلقة، أو سلوك غير متوقع).
  • تعتمد الوظيفة على الانتظار المشغول.وهذا يعد إهدارًا فظيعًا نظرًا لأنه من المتوقع أن يستمر الانتظار في أي مكان من عدة ثوانٍ حتى انتهاء المهلة، والتي قد تكون عدة دقائق.يعد الانتظار المزدحم الذي يستمر لمثل هذا القدر من الوقت بمثابة استنزاف فظيع للموارد، وهو أمر سيء بشكل خاص في سيناريو تعدد العمليات.إذا تم تعديل حالة الانتظار المزدحمة بالنوم، فهذا له تأثير سلبي على الاستجابة، على الرغم من أنني أعترف بأن هذه ربما لا تكون مشكلة كبيرة.

أعتقد أن الحل الذي قدمته سيحل المشكلة الأصلية دون المعاناة من أي من المشكلات المذكورة أعلاه:

class Reader {
  private static Thread inputThread;
  private static AutoResetEvent getInput, gotInput;
  private static string input;

  static Reader() {
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(reader);
    inputThread.IsBackground = true;
    inputThread.Start();
  }

  private static void reader() {
    while (true) {
      getInput.WaitOne();
      input = Console.ReadLine();
      gotInput.Set();
    }
  }

  // omit the parameter to read a line without a timeout
  public static string ReadLine(int timeOutMillisecs = Timeout.Infinite) {
    getInput.Set();
    bool success = gotInput.WaitOne(timeOutMillisecs);
    if (success)
      return input;
    else
      throw new TimeoutException("User did not provide input within the timelimit.");
  }
}

المكالمة بالطبع سهلة للغاية:

try {
  Console.WriteLine("Please enter your name within the next 5 seconds.");
  string name = Reader.ReadLine(5000);
  Console.WriteLine("Hello, {0}!", name);
} catch (TimeoutException) {
  Console.WriteLine("Sorry, you waited too long.");
}

وبدلاً من ذلك، يمكنك استخدام TryXX(out) الاتفاقية ، كما اقترح شموئيلي:

  public static bool TryReadLine(out string line, int timeOutMillisecs = Timeout.Infinite) {
    getInput.Set();
    bool success = gotInput.WaitOne(timeOutMillisecs);
    if (success)
      line = input;
    else
      line = null;
    return success;
  }

والذي يسمى على النحو التالي:

Console.WriteLine("Please enter your name within the next 5 seconds.");
string name;
bool success = Reader.TryReadLine(out name, 5000);
if (!success)
  Console.WriteLine("Sorry, you waited too long.");
else
  Console.WriteLine("Hello, {0}!", name);

وفي كلتا الحالتين، لا يمكنك الخلط بين المكالمات Reader مع عادي Console.ReadLine المكالمات:إذا Reader بعد انتهاء المهلة، سيكون هناك تعليق ReadLine يتصل.بدلاً من ذلك، إذا كنت تريد الحصول على عرض عادي (غير محدد التوقيت) ReadLine اتصل، فقط استخدم Reader وحذف المهلة، بحيث يتم تعيينها افتراضيًا على مهلة لا نهائية.

إذن ماذا عن مشاكل الحلول الأخرى التي ذكرتها؟

  • كما ترون، تم استخدام ReadLine لتجنب المشكلة الأولى.
  • تعمل الوظيفة بشكل صحيح عند استدعائها عدة مرات.بغض النظر عما إذا كانت المهلة قد انتهت أم لا، سيتم تشغيل مؤشر ترابط واحد فقط في الخلفية ولن يتم تنشيط سوى استدعاء واحد على الأكثر لـ ReadLine.سيؤدي استدعاء الوظيفة دائمًا إلى إدخال أحدث إدخال، أو انتهاء المهلة، ولن يضطر المستخدم إلى الضغط على زر الإدخال أكثر من مرة لإرسال إدخاله.
  • ومن الواضح أن الوظيفة لا تعتمد على الانتظار المزدحم.وبدلاً من ذلك يستخدم تقنيات تعدد العمليات المناسبة لمنع إهدار الموارد.

المشكلة الوحيدة التي أتوقعها مع هذا الحل هي أنه ليس آمنًا للخيط.ومع ذلك، لا يمكن لسلاسل المحادثات المتعددة أن تطلب من المستخدم الإدخال في نفس الوقت، لذلك يجب أن تتم المزامنة قبل إجراء اتصال بـ Reader.ReadLine على أي حال.

نصائح أخرى

string ReadLine(int timeoutms)
{
    ReadLineDelegate d = Console.ReadLine;
    IAsyncResult result = d.BeginInvoke(null, null);
    result.AsyncWaitHandle.WaitOne(timeoutms);//timeout e.g. 15000 for 15 secs
    if (result.IsCompleted)
    {
        string resultstr = d.EndInvoke(result);
        Console.WriteLine("Read: " + resultstr);
        return resultstr;
    }
    else
    {
        Console.WriteLine("Timed out!");
        throw new TimedoutException("Timed Out!");
    }
}

delegate string ReadLineDelegate();

هل هذا النهج باستخدام Console.KeyAvailable يساعد؟

class Sample 
{
    public static void Main() 
    {
    ConsoleKeyInfo cki = new ConsoleKeyInfo();

    do {
        Console.WriteLine("\nPress a key to display; press the 'x' key to quit.");

// Your code could perform some useful task in the following loop. However, 
// for the sake of this example we'll merely pause for a quarter second.

        while (Console.KeyAvailable == false)
            Thread.Sleep(250); // Loop until input is entered.
        cki = Console.ReadKey(true);
        Console.WriteLine("You pressed the '{0}' key.", cki.Key);
        } while(cki.Key != ConsoleKey.X);
    }
}

بطريقة أو بأخرى، أنت بحاجة إلى خيط ثانٍ.يمكنك استخدام عمليات الإدخال والإخراج غير المتزامنة لتجنب الإعلان عن بياناتك الخاصة:

  • أعلن عن ManualResetEvent، وأطلق عليه اسم "evt"
  • اتصل بـ System.Console.OpenStandardInput للحصول على دفق الإدخال.حدد طريقة رد الاتصال التي ستقوم بتخزين بياناتها وتعيين evt.
  • قم باستدعاء أسلوب BeginRead الخاص بهذا الدفق لبدء عملية قراءة غير متزامنة
  • ثم أدخل الانتظار المحدد بوقت على ManualResetEvent
  • إذا انتهت مهلة الانتظار، فقم بإلغاء القراءة

إذا أعادت القراءة البيانات، فقم بتعيين الحدث وسيستمر موضوعك الرئيسي، وإلا فسوف تستمر بعد انتهاء المهلة.

// Wait for 'Enter' to be pressed or 5 seconds to elapse
using (Stream s = Console.OpenStandardInput())
{
    ManualResetEvent stop_waiting = new ManualResetEvent(false);
    s.BeginRead(new Byte[1], 0, 1, ar => stop_waiting.Set(), null);

    // ...do anything else, or simply...

    stop_waiting.WaitOne(5000);
    // If desired, other threads could also set 'stop_waiting' 
    // Disposing the stream cancels the async read operation. It can be
    // re-opened if needed.
}

لقد نجح هذا بالنسبة لي.

ConsoleKeyInfo k = new ConsoleKeyInfo();
Console.WriteLine("Press any key in the next 5 seconds.");
for (int cnt = 5; cnt > 0; cnt--)
  {
    if (Console.KeyAvailable == true)
      {
        k = Console.ReadKey();
        break;
      }
    else
     {
       Console.WriteLine(cnt.ToString());
       System.Threading.Thread.Sleep(1000);
     }
 }
Console.WriteLine("The key pressed was " + k.Key);

أعتقد أنك ستحتاج إلى إنشاء موضوع ثانوي واستطلاع لمفتاح على وحدة التحكم.لا أعرف أي طريقة مدمجة لتحقيق ذلك.

لقد عانيت من هذه المشكلة لمدة 5 أشهر قبل أن أجد حلاً يعمل بشكل مثالي في بيئة المؤسسة.

المشكلة في معظم الحلول حتى الآن هي أنها تعتمد على شيء آخر غير Console.ReadLine()، وConsole.ReadLine() لها الكثير من المزايا:

  • دعم الحذف، مسافة للخلف، مفاتيح الأسهم، الخ.
  • القدرة على الضغط على المفتاح "لأعلى" وتكرار الأمر الأخير (يكون هذا مفيدًا جدًا إذا قمت بتطبيق وحدة تحكم لتصحيح أخطاء الخلفية والتي تحظى بالكثير من الاستخدام).

الحل الخاص بي هو كما يلي:

  1. تفرخ أ موضوع منفصل للتعامل مع إدخال المستخدم باستخدام Console.ReadLine().
  2. بعد انقضاء فترة المهلة، قم بإلغاء حظر Console.ReadLine() عن طريق إرسال مفتاح [enter] إلى نافذة وحدة التحكم الحالية، باستخدام http://inputsimulator.codeplex.com/.

عينة من الرموز:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

مزيد من المعلومات حول هذه التقنية، بما في ذلك التقنية الصحيحة لإحباط مؤشر ترابط يستخدم Console.ReadLine:

استدعاء .NET لإرسال [إدخال] ضغطة مفتاح إلى العملية الحالية، ما هو تطبيق وحدة التحكم؟

كيفية إحباط مؤشر ترابط آخر في .NET، عندما يقوم مؤشر الترابط المذكور بتنفيذ Console.ReadLine؟

يعد استدعاء Console.ReadLine() في المفوض أمرًا سيئًا لأنه إذا لم يضغط المستخدم على "إدخال"، فلن يعود هذا الاتصال أبدًا.سيتم حظر الخيط الذي ينفذ المفوض حتى يضغط المستخدم على "إدخال"، دون أي وسيلة لإلغائه.

إصدار سلسلة من هذه المكالمات لن يتصرف كما تتوقع.خذ بعين الاعتبار ما يلي (باستخدام فئة وحدة التحكم المثال من أعلاه):

System.Console.WriteLine("Enter your first name [John]:");

string firstName = Console.ReadLine(5, "John");

System.Console.WriteLine("Enter your last name [Doe]:");

string lastName = Console.ReadLine(5, "Doe");

يسمح المستخدم بانتهاء المهلة للموجه الأول، ثم يُدخل قيمة للموجه الثاني.سيحتوي كل من الاسم الأول واسم العائلة على القيم الافتراضية.عندما يضغط المستخدم على "أدخل"، أولاً سيكتمل استدعاء ReadLine، لكن الكود قد تخلى عن هذا الاستدعاء وتجاهل النتيجة بشكل أساسي.ال ثانية سيستمر حظر استدعاء ReadLine، وستنتهي المهلة في النهاية وستكون القيمة التي تم إرجاعها هي القيمة الافتراضية مرة أخرى.

راجع للشغل - يوجد خطأ في الكود أعلاه.من خلال استدعاء waitHandle.Close()، يمكنك إغلاق الحدث من أسفل مؤشر الترابط العامل.إذا قام المستخدم بالضغط على "إدخال" بعد انتهاء المهلة، فسيحاول مؤشر ترابط العامل الإشارة إلى الحدث الذي يطرح ObjectDisposeException.يتم طرح الاستثناء من مؤشر ترابط العامل، وإذا لم تقم بإعداد معالج استثناء غير معالج، فسيتم إنهاء العملية.

ربما أقرأ الكثير في السؤال، لكنني أفترض أن الانتظار سيكون مشابهًا لقائمة التمهيد حيث تنتظر 15 ثانية ما لم تضغط على مفتاح.يمكنك إما استخدام (1) وظيفة الحظر أو (2) يمكنك استخدام سلسلة رسائل وحدث ومؤقت.سيكون الحدث بمثابة "متابعة" وسيتم حظره حتى انتهاء صلاحية المؤقت أو الضغط على المفتاح.

الرمز الزائف لـ (1) سيكون:

// Get configurable wait time
TimeSpan waitTime = TimeSpan.FromSeconds(15.0);
int configWaitTimeSec;
if (int.TryParse(ConfigManager.AppSetting["DefaultWaitTime"], out configWaitTimeSec))
    waitTime = TimeSpan.FromSeconds(configWaitTimeSec);

bool keyPressed = false;
DateTime expireTime = DateTime.Now + waitTime;

// Timer and key processor
ConsoleKeyInfo cki;
// EDIT: adding a missing ! below
while (!keyPressed && (DateTime.Now < expireTime))
{
    if (Console.KeyAvailable)
    {
        cki = Console.ReadKey(true);
        // TODO: Process key
        keyPressed = true;
    }
    Thread.Sleep(10);
}

إذا كنت في Main() الطريقة، لا يمكنك استخدامها await, ، لذلك عليك أن تستخدم Task.WaitAny():

var task = Task.Factory.StartNew(Console.ReadLine);
var result = Task.WaitAny(new Task[] { task }, TimeSpan.FromSeconds(5)) == 0
    ? task.Result : string.Empty;

ومع ذلك، يقدم الإصدار C# 7.1 إمكانية إنشاء ملف غير متزامن Main() الطريقة، لذلك فمن الأفضل استخدام Task.WhenAny() الإصدار عندما يكون لديك هذا الخيار:

var task = Task.Factory.StartNew(Console.ReadLine);
var completedTask = await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(5)));
var result = object.ReferenceEquals(task, completedTask) ? task.Result : string.Empty;

لا أستطيع التعليق على مشاركة جولزار للأسف، ولكن إليك مثال أكمل:

            while (Console.KeyAvailable == false)
            {
                Thread.Sleep(250);
                i++;
                if (i > 3)
                    throw new Exception("Timedout waiting for input.");
            }
            input = Console.ReadLine();

يحرر:تم إصلاح المشكلة من خلال إنجاز العمل الفعلي في عملية منفصلة وإنهاء هذه العملية في حالة انتهاء مهلةها.انظر أدناه للحصول على التفاصيل.يا للعجب!

لقد قمت للتو بتجربة هذا ويبدو أنه يعمل بشكل جيد.كان لدى زميلي في العمل إصدار يستخدم كائن Thread، ولكنني أجد أن طريقة BeginInvoc() لأنواع المفوضين أكثر أناقة قليلاً.

namespace TimedReadLine
{
   public static class Console
   {
      private delegate string ReadLineInvoker();

      public static string ReadLine(int timeout)
      {
         return ReadLine(timeout, null);
      }

      public static string ReadLine(int timeout, string @default)
      {
         using (var process = new System.Diagnostics.Process
         {
            StartInfo =
            {
               FileName = "ReadLine.exe",
               RedirectStandardOutput = true,
               UseShellExecute = false
            }
         })
         {
            process.Start();

            var rli = new ReadLineInvoker(process.StandardOutput.ReadLine);
            var iar = rli.BeginInvoke(null, null);

            if (!iar.AsyncWaitHandle.WaitOne(new System.TimeSpan(0, 0, timeout)))
            {
               process.Kill();
               return @default;
            }

            return rli.EndInvoke(iar);
         }
      }
   }
}

يعد مشروع ReadLine.exe مشروعًا بسيطًا جدًا ويحتوي على فئة واحدة تبدو كما يلي:

namespace ReadLine
{
   internal static class Program
   {
      private static void Main()
      {
         System.Console.WriteLine(System.Console.ReadLine());
      }
   }
}

.NET 4 يجعل هذا الأمر بسيطًا للغاية باستخدام المهام.

أولاً، قم ببناء مساعدك:

   Private Function AskUser() As String
      Console.Write("Answer my question: ")
      Return Console.ReadLine()
   End Function

ثانيًا، قم بتنفيذ المهمة وانتظر:

      Dim askTask As Task(Of String) = New TaskFactory().StartNew(Function() AskUser())
      askTask.Wait(TimeSpan.FromSeconds(30))
      If Not askTask.IsCompleted Then
         Console.WriteLine("User failed to respond.")
      Else
         Console.WriteLine(String.Format("You responded, '{0}'.", askTask.Result))
      End If

ليست هناك محاولة لإعادة إنشاء وظيفة ReadLine أو تنفيذ عمليات اختراق محفوفة بالمخاطر لتفعيل هذا الأمر.تتيح لنا المهام حل السؤال بطريقة طبيعية جدًا.

كما لو لم تكن هناك إجابات كافية هنا بالفعل:0)، يتم تضمين ما يلي في طريقة ثابتة لحل @kwl أعلاه (الحل الأول).

    public static string ConsoleReadLineWithTimeout(TimeSpan timeout)
    {
        Task<string> task = Task.Factory.StartNew(Console.ReadLine);

        string result = Task.WaitAny(new Task[] { task }, timeout) == 0
            ? task.Result 
            : string.Empty;
        return result;
    }

الاستخدام

    static void Main()
    {
        Console.WriteLine("howdy");
        string result = ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(8.5));
        Console.WriteLine("bye");
    }

مثال خيوط بسيط لحل هذه المشكلة

Thread readKeyThread = new Thread(ReadKeyMethod);
static ConsoleKeyInfo cki = null;

void Main()
{
    readKeyThread.Start();
    bool keyEntered = false;
    for(int ii = 0; ii < 10; ii++)
    {
        Thread.Sleep(1000);
        if(readKeyThread.ThreadState == ThreadState.Stopped)
            keyEntered = true;
    }
    if(keyEntered)
    { //do your stuff for a key entered
    }
}

void ReadKeyMethod()
{
    cki = Console.ReadKey();
}

أو سلسلة ثابتة لأعلى للحصول على سطر كامل.

أنا حالتي هذا يعمل بشكل جيد:

public static ManualResetEvent evtToWait = new ManualResetEvent(false);

private static void ReadDataFromConsole( object state )
{
    Console.WriteLine("Enter \"x\" to exit or wait for 5 seconds.");

    while (Console.ReadKey().KeyChar != 'x')
    {
        Console.Out.WriteLine("");
        Console.Out.WriteLine("Enter again!");
    }

    evtToWait.Set();
}

static void Main(string[] args)
{
        Thread status = new Thread(ReadDataFromConsole);
        status.Start();

        evtToWait = new ManualResetEvent(false);

        evtToWait.WaitOne(5000); // wait for evtToWait.Set() or timeOut

        status.Abort(); // exit anyway
        return;
}

أليس هذا لطيفا وقصيرا؟

if (SpinWait.SpinUntil(() => Console.KeyAvailable, millisecondsTimeout))
{
    ConsoleKeyInfo keyInfo = Console.ReadKey();

    // Handle keyInfo value here...
}

هذا مثال أكمل لحل جلين سلايدن.لقد حدث ذلك عند إنشاء حالة اختبار لمشكلة أخرى.يستخدم الإدخال / الإخراج غير المتزامن وحدث إعادة التعيين اليدوي.

public static void Main() {
    bool readInProgress = false;
    System.IAsyncResult result = null;
    var stop_waiting = new System.Threading.ManualResetEvent(false);
    byte[] buffer = new byte[256];
    var s = System.Console.OpenStandardInput();
    while (true) {
        if (!readInProgress) {
            readInProgress = true;
            result = s.BeginRead(buffer, 0, buffer.Length
              , ar => stop_waiting.Set(), null);

        }
        bool signaled = true;
        if (!result.IsCompleted) {
            stop_waiting.Reset();
            signaled = stop_waiting.WaitOne(5000);
        }
        else {
            signaled = true;
        }
        if (signaled) {
            readInProgress = false;
            int numBytes = s.EndRead(result);
            string text = System.Text.Encoding.UTF8.GetString(buffer
              , 0, numBytes);
            System.Console.Out.Write(string.Format(
              "Thank you for typing: {0}", text));
        }
        else {
            System.Console.Out.WriteLine("oy, type something!");
        }
    }

هناك طريقة أخرى رخيصة للحصول على خيط ثانٍ وهي لفه في مندوب.

مثال على تنفيذ مشاركة إريك أعلاه.تم استخدام هذا المثال تحديدًا لقراءة المعلومات التي تم تمريرها إلى تطبيق وحدة التحكم عبر توجيه الإخراج:

 using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace PipedInfo
{
    class Program
    {
        static void Main(string[] args)
        {
            StreamReader buffer = ReadPipedInfo();

            Console.WriteLine(buffer.ReadToEnd());
        }

        #region ReadPipedInfo
        public static StreamReader ReadPipedInfo()
        {
            //call with a default value of 5 milliseconds
            return ReadPipedInfo(5);
        }

        public static StreamReader ReadPipedInfo(int waitTimeInMilliseconds)
        {
            //allocate the class we're going to callback to
            ReadPipedInfoCallback callbackClass = new ReadPipedInfoCallback();

            //to indicate read complete or timeout
            AutoResetEvent readCompleteEvent = new AutoResetEvent(false);

            //open the StdIn so that we can read against it asynchronously
            Stream stdIn = Console.OpenStandardInput();

            //allocate a one-byte buffer, we're going to read off the stream one byte at a time
            byte[] singleByteBuffer = new byte[1];

            //allocate a list of an arbitary size to store the read bytes
            List<byte> byteStorage = new List<byte>(4096);

            IAsyncResult asyncRead = null;
            int readLength = 0; //the bytes we have successfully read

            do
            {
                //perform the read and wait until it finishes, unless it's already finished
                asyncRead = stdIn.BeginRead(singleByteBuffer, 0, singleByteBuffer.Length, new AsyncCallback(callbackClass.ReadCallback), readCompleteEvent);
                if (!asyncRead.CompletedSynchronously)
                    readCompleteEvent.WaitOne(waitTimeInMilliseconds);

                //end the async call, one way or another

                //if our read succeeded we store the byte we read
                if (asyncRead.IsCompleted)
                {
                    readLength = stdIn.EndRead(asyncRead);
                    if (readLength > 0)
                        byteStorage.Add(singleByteBuffer[0]);
                }

            } while (asyncRead.IsCompleted && readLength > 0);
            //we keep reading until we fail or read nothing

            //return results, if we read zero bytes the buffer will return empty
            return new StreamReader(new MemoryStream(byteStorage.ToArray(), 0, byteStorage.Count));
        }

        private class ReadPipedInfoCallback
        {
            public void ReadCallback(IAsyncResult asyncResult)
            {
                //pull the user-defined variable and strobe the event, the read finished successfully
                AutoResetEvent readCompleteEvent = asyncResult.AsyncState as AutoResetEvent;
                readCompleteEvent.Set();
            }
        }
        #endregion ReadPipedInfo
    }
}
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

لاحظ أنه إذا سلكت طريق "Console.ReadKey"، فإنك ستفقد بعض الميزات الرائعة لـ ReadLine، وهي:

  • دعم الحذف، مسافة للخلف، مفاتيح الأسهم، الخ.
  • القدرة على الضغط على المفتاح "لأعلى" وتكرار الأمر الأخير (يكون هذا مفيدًا جدًا إذا قمت بتطبيق وحدة تحكم لتصحيح أخطاء الخلفية والتي تحظى بالكثير من الاستخدام).

لإضافة مهلة، قم بتغيير حلقة while لتناسبك.

من فضلك لا تكرهني لإضافة حل آخر لعدد كبير من الإجابات الموجودة!يعمل هذا مع Console.ReadKey()، ولكن يمكن تعديله بسهولة للعمل مع ReadLine()، وما إلى ذلك.

نظرًا لأن طرق "Console.Read" محظورة، فمن الضروري "دفعة" دفق StdIn لإلغاء القراءة.

بناء جملة الاستدعاء:

ConsoleKeyInfo keyInfo;
bool keyPressed = AsyncConsole.ReadKey(500, out keyInfo);
// where 500 is the timeout

شفرة:

public class AsyncConsole // not thread safe
{
    private static readonly Lazy<AsyncConsole> Instance =
        new Lazy<AsyncConsole>();

    private bool _keyPressed;
    private ConsoleKeyInfo _keyInfo;

    private bool DoReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        _keyPressed = false;
        _keyInfo = new ConsoleKeyInfo();

        Thread readKeyThread = new Thread(ReadKeyThread);
        readKeyThread.IsBackground = false;
        readKeyThread.Start();

        Thread.Sleep(millisecondsTimeout);

        if (readKeyThread.IsAlive)
        {
            try
            {
                IntPtr stdin = GetStdHandle(StdHandle.StdIn);
                CloseHandle(stdin);
                readKeyThread.Join();
            }
            catch { }
        }

        readKeyThread = null;

        keyInfo = _keyInfo;
        return _keyPressed;
    }

    private void ReadKeyThread()
    {
        try
        {
            _keyInfo = Console.ReadKey();
            _keyPressed = true;
        }
        catch (InvalidOperationException) { }
    }

    public static bool ReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        return Instance.Value.DoReadKey(millisecondsTimeout, out keyInfo);
    }

    private enum StdHandle { StdIn = -10, StdOut = -11, StdErr = -12 };

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetStdHandle(StdHandle std);

    [DllImport("kernel32.dll")]
    private static extern bool CloseHandle(IntPtr hdl);
}

هنا الحل الذي يستخدم Console.KeyAvailable.تعمل هذه على حظر المكالمات، ولكن يجب أن يكون من السهل جدًا الاتصال بها بشكل غير متزامن عبر TPL إذا رغبت في ذلك.لقد استخدمت آليات الإلغاء القياسية لتسهيل الاتصال بنمط المهام غير المتزامن وكل تلك الأشياء الجيدة.

public static class ConsoleEx
{
  public static string ReadLine(TimeSpan timeout)
  {
    var cts = new CancellationTokenSource();
    return ReadLine(timeout, cts.Token);
  }

  public static string ReadLine(TimeSpan timeout, CancellationToken cancellation)
  {
    string line = "";
    DateTime latest = DateTime.UtcNow.Add(timeout);
    do
    {
        cancellation.ThrowIfCancellationRequested();
        if (Console.KeyAvailable)
        {
            ConsoleKeyInfo cki = Console.ReadKey();
            if (cki.Key == ConsoleKey.Enter)
            {
                return line;
            }
            else
            {
                line += cki.KeyChar;
            }
        }
        Thread.Sleep(1);
    }
    while (DateTime.UtcNow < latest);
    return null;
  }
}

هناك بعض العيوب مع هذا.

  • لا تحصل على ميزات التنقل القياسية التي ReadLine يوفر (التمرير بالسهم لأعلى/لأسفل، وما إلى ذلك).
  • يؤدي هذا إلى إدخال الأحرف '\0' في الإدخال إذا تم الضغط على مفتاح خاص (F1، PrtScn، وما إلى ذلك).يمكنك تصفيتها بسهولة عن طريق تعديل الكود.

انتهى الأمر هنا لأنه تم طرح سؤال مكرر.لقد توصلت إلى الحل التالي الذي يبدو واضحًا.أنا متأكد من أن لديه بعض العيوب فاتني.

static void Main(string[] args)
{
    Console.WriteLine("Hit q to continue or wait 10 seconds.");

    Task task = Task.Factory.StartNew(() => loop());

    Console.WriteLine("Started waiting");
    task.Wait(10000);
    Console.WriteLine("Stopped waiting");
}

static void loop()
{
    while (true)
    {
        if ('q' == Console.ReadKey().KeyChar) break;
    }
}

جئت إلى هذه الإجابة وانتهى بي الأمر:

    /// <summary>
    /// Reads Line from console with timeout. 
    /// </summary>
    /// <exception cref="System.TimeoutException">If user does not enter line in the specified time.</exception>
    /// <param name="timeout">Time to wait in milliseconds. Negative value will wait forever.</param>        
    /// <returns></returns>        
    public static string ReadLine(int timeout = -1)
    {
        ConsoleKeyInfo cki = new ConsoleKeyInfo();
        StringBuilder sb = new StringBuilder();

        // if user does not want to spesify a timeout
        if (timeout < 0)
            return Console.ReadLine();

        int counter = 0;

        while (true)
        {
            while (Console.KeyAvailable == false)
            {
                counter++;
                Thread.Sleep(1);
                if (counter > timeout)
                    throw new System.TimeoutException("Line was not entered in timeout specified");
            }

            cki = Console.ReadKey(false);

            if (cki.Key == ConsoleKey.Enter)
            {
                Console.WriteLine();
                return sb.ToString();
            }
            else
                sb.Append(cki.KeyChar);                
        }            
    }

مثال بسيط باستخدام Console.KeyAvailable:

Console.WriteLine("Press any key during the next 2 seconds...");
Thread.Sleep(2000);
if (Console.KeyAvailable)
{
    Console.WriteLine("Key pressed");
}
else
{
    Console.WriteLine("You were too slow");
}

سيبدو الكثير من التعليمات البرمجية المعاصرة والمعتمدة على المهام كما يلي:

public string ReadLine(int timeOutMillisecs)
{
    var inputBuilder = new StringBuilder();

    var task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            var consoleKey = Console.ReadKey(true);
            if (consoleKey.Key == ConsoleKey.Enter)
            {
                return inputBuilder.ToString();
            }

            inputBuilder.Append(consoleKey.KeyChar);
        }
    });


    var success = task.Wait(timeOutMillisecs);
    if (!success)
    {
        throw new TimeoutException("User did not provide input within the timelimit.");
    }

    return inputBuilder.ToString();
}

كان لدي موقف فريد من نوعه حيث كان لدي تطبيق Windows (خدمة Windows).عند تشغيل البرنامج بشكل تفاعلي Environment.IsInteractive (VS Debugger أو من cmd.exe)، استخدمت AttachConsole/AllocConsole للحصول على stdin/stdout.لمنع انتهاء العملية أثناء إنجاز العمل، يستدعي مؤشر ترابط واجهة المستخدم Console.ReadKey(false).كنت أرغب في إلغاء الانتظار الذي كان يقوم به مؤشر ترابط واجهة المستخدم من مؤشر ترابط آخر، لذلك توصلت إلى تعديل للحل بواسطةJSquaredD.

using System;
using System.Diagnostics;

internal class PressAnyKey
{
  private static Thread inputThread;
  private static AutoResetEvent getInput;
  private static AutoResetEvent gotInput;
  private static CancellationTokenSource cancellationtoken;

  static PressAnyKey()
  {
    // Static Constructor called when WaitOne is called (technically Cancel too, but who cares)
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(ReaderThread);
    inputThread.IsBackground = true;
    inputThread.Name = "PressAnyKey";
    inputThread.Start();
  }

  private static void ReaderThread()
  {
    while (true)
    {
      // ReaderThread waits until PressAnyKey is called
      getInput.WaitOne();
      // Get here 
      // Inner loop used when a caller uses PressAnyKey
      while (!Console.KeyAvailable && !cancellationtoken.IsCancellationRequested)
      {
        Thread.Sleep(50);
      }
      // Release the thread that called PressAnyKey
      gotInput.Set();
    }
  }

  /// <summary>
  /// Signals the thread that called WaitOne should be allowed to continue
  /// </summary>
  public static void Cancel()
  {
    // Trigger the alternate ending condition to the inner loop in ReaderThread
    if(cancellationtoken== null) throw new InvalidOperationException("Must call WaitOne before Cancelling");
    cancellationtoken.Cancel();
  }

  /// <summary>
  /// Wait until a key is pressed or <see cref="Cancel"/> is called by another thread
  /// </summary>
  public static void WaitOne()
  {
    if(cancellationtoken==null || cancellationtoken.IsCancellationRequested) throw new InvalidOperationException("Must cancel a pending wait");
    cancellationtoken = new CancellationTokenSource();
    // Release the reader thread
    getInput.Set();
    // Calling thread will wait here indefiniately 
    // until a key is pressed, or Cancel is called
    gotInput.WaitOne();
  }    
}

يبدو أن هذا هو الحل الأبسط والعملي الذي لا يستخدم أي واجهات برمجة تطبيقات أصلية:

    static Task<string> ReadLineAsync(CancellationToken cancellation)
    {
        return Task.Run(() =>
        {
            while (!Console.KeyAvailable)
            {
                if (cancellation.IsCancellationRequested)
                    return null;

                Thread.Sleep(100);
            }
            return Console.ReadLine();
        });
    }

مثال على الاستخدام:

    static void Main(string[] args)
    {
        AsyncContext.Run(async () =>
        {
            CancellationTokenSource cancelSource = new CancellationTokenSource();
            cancelSource.CancelAfter(1000);
            Console.WriteLine(await ReadLineAsync(cancelSource.Token) ?? "null");
        });
    }
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top