为什么在第一次输出后控制台冻结输出?

Why output on console freeze after first output?

这里我有用 C# 编写的简单代码。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Reactive.Subjects;

namespace ReactiveProgramming
{
    class Program
    {

        static void Main(string[] args)
        {
            var generateSeq = new GenerateSequence();
            Console.WriteLine("Hello World!");

            generateSeq.Sequence.Subscribe(val =>
            {
                Console.WriteLine(val);

                // it works if I remove below two lines ...
                Console.SetCursorPosition(0, Console.CursorTop - 1);   
                Console.Write("\r" + new string(' ', Console.WindowWidth) + "\r");
            });

            generateSeq.Run();
        }
    }

    class GenerateSequence
    {
        public Subject<int> Sequence = new Subject<int>();

        public void Run(int runTimes = 10)
        {
            ConsoleKeyInfo cki;

            Task.Run(() => runForTimes(10));

            do
            {
                cki = Console.ReadKey();
            } while (cki.Key != ConsoleKey.Escape);
        }

        public void runForTimes(int runTimes = 10)
        {
            for (var i = 0; i < 10; i++)
            {
                Sequence.OnNext(i);
                Thread.Sleep(1000);
            }
        }
    }
}

但是它不是在彼此之上打印序列,而是在第一次发出后冻结输出。

也在 Linux 中进行了测试...同样的输出。

如果我将这些行 Console.SetCursorPositionConsole.Write("\r" + new string(' ', Console.WindowWidth) + "\r") 从 subscribe 中远程...它可以工作并在屏幕上一个接一个地打印所有数字,但我想一个接一个地打印...

但是如果我像这样更改我的 Main 函数:

    static void Main(string[] args)
    {
        var generateSeq = new GenerateSequence();
        Console.WriteLine("Hello World!");

        generateSeq.Sequence.Subscribe(val =>
        {
            Console.WriteLine(val);
            // Console.SetCursorPosition(0, Console.CursorTop - 1);
            // Console.Write("\r" + new string(' ', Console.WindowWidth) + "\r");
        });

        generateSeq.Run();
    }

我注释掉这两行的地方...输出如下...

但是我不想像第二张图片那样按顺序输出,我想在同一个位置打印输出。将新输出覆盖在旧输出上

注意:我在 Macbook Pro (Big Sur) 上 运行,它发生在 .net core 3.1 或 .net 5.0 并使用 iTerm 作为控制台模拟器

SetCursorPosition 在不从另一个线程调用时工作得很好。您可以使用异步方法来解决问题,而不是使用 Task.Run

  class Program
    {
        static void Main(string[] args)
        {
            var generateSeq = new GenerateSequence();
            Console.WriteLine("Hello World!");

            generateSeq.Sequence.Subscribe(val =>
            {
                Console.WriteLine(val);
                // move cursor back to previous line
                Console.SetCursorPosition(0 ,Console.CursorTop - 1);
            });
            
            // start background operation
            generateSeq.Run();
        }
    }
    
    class GenerateSequence
    {
        public readonly Subject<int> Sequence = new();

        public void Run(int runTimes = 10)
        {
            ConsoleKeyInfo cki;

            // create a cancelation token, because if the user presses
            // Escape key we don't need to run our background task 
            // anymore and the task should be stopped.
            var tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;

            // we can not use await keyword here because we need to 
            // listen to ReadKey functions in case the user wants to 
            // stop the execution. without the await, task will run in 
            // the background asynchronously
            var task = RunForTimes(runTimes,token);

            // wait for the Escape key to cancel the execution or stop it 
            // if it's already running
            do
            {
                cki = Console.ReadKey();
            } while (cki.Key != ConsoleKey.Escape && !task.IsCompleted);

            // cancel the background task if it's not compeleted.
            if (!task.IsCompleted)
                tokenSource.Cancel();    
            
            // Revert CursorPosition to the original state
            Console.SetCursorPosition(0, Console.CursorTop + 1); 

            Console.WriteLine("Execution ends");
        }

        // we use an async task instead of a void to run our background 
        // job Asynchronously.
        // the main difference is, we should not use a separate thread 
        // because we need to be on the main thread to safely access the Console (to read or write)
        private async Task RunForTimes(int runTimes, CancellationToken token)
        {
            for (var i = 0; i < runTimes; i++)
            {
                Sequence.OnNext(i);
                await Task.Delay(1000, token);
             // exit the operation if it is requested
                if (token.IsCancellationRequested) return;
            }
        }
    }

如果我正在写这篇文章,我会选择这个实现:

    static async Task Main(string[] args)
    {
        Console.WriteLine("Hello World!");

        IObservable<System.ConsoleKeyInfo> keys =
            Observable
                .Start(() => Console.ReadKey());

        await
            Observable
                .Interval(TimeSpan.FromSeconds(1.0))
                .Take(10)
                .TakeUntil(keys)
                .Do(x =>
                {
                    Console.WriteLine(x);
                    Console.SetCursorPosition(0, Console.CursorTop - 1);
                },
                () => Console.SetCursorPosition(0, Console.CursorTop + 1));

        Console.WriteLine("Bye World!");
    }

尽可能避免使用主题。