要使弹出对话框居中于父窗口的中心,您可以实现自己的对话框并使用它代替MessageBox
. 但有时这是多余的,您需要以一种简单而熟悉的方式使用它,而且如果您有一个旧项目,您可以改进它的行为而无需花费大量时间进行修改。
MessageBox
默认弹出在屏幕中央,但是如何让它在窗口中央呢?
要使弹出对话框居中于父窗口的中心,您可以实现自己的对话框并使用它代替MessageBox
. 但有时这是多余的,您需要以一种简单而熟悉的方式使用它,而且如果您有一个旧项目,您可以改进它的行为而无需花费大量时间进行修改。
MessageBox
默认弹出在屏幕中央,但是如何让它在窗口中央呢?
每个人都知道没有异步构造函数,好吧,至少现在还没有。该怎么办?
构造函数中已经有一个类似的问题异步代码,尽管它下面有许多无条件有用的注释,但该问题的解决方案仅针对工业规模提供 -正确和复杂。
我坐下来困惑于如何最简单安全地初始化一个直到初始化对象内部状态的异步操作结束才能使用的对象。
不符合上述条件的最明显的正面解决方案:
public class MyClass
{
private int _data;
public async Task InitAsync()
{
await Task.Delay(100);
data = 42;
}
public int GetData()
{
if (_data != 42)
throw new Exception("Объект не инициализирован, запустите InitAsync(), и дождитесь его завершения");
return _data;
}
}
即在调用worker方法时,检查对象是否初始化正确,否则抛出异常。
var instance = new MyClass();
await instance.InitAsync();
int result = instance.GetData();
InitAsync()
该解决方案是有效的,但如果开发人员忘记调用或由于其他原因未调用此方法,或者在初始化方法中发生异常,则容易出现开发错误。一般来说,有机会获得非工作副本,这很糟糕。
有没有一种简单的方法来实施一致的解决方案?没有工厂、大量接口和其他东西。
我在整理这个问题时遇到了魔法,顺便说一下,我无法重现作者所写的问题。
我拿了我的 TCP 服务器的代码。我把它全部带到这里。试图创建一个最小的可重现示例,但无法重现该问题。
class Program
{
public static async Task Main(string[] args)
{
int port = 85;
Console.WriteLine("Запуск сервера....");
using (TcpServer server = new TcpServer(port))
{
Task servertask = server.ListenAsync();
Console.WriteLine($"Слушаем клиента на порту: {port}");
while (true)
{
string input = Console.ReadLine();
if (input == "stop")
{
Console.WriteLine("Остановка сервера...");
server.Stop();
break;
}
}
await servertask;
}
Console.WriteLine("Нажмите любую клавишу для выхода...");
Console.ReadKey(true);
}
}
class TcpServer : IDisposable
{
private readonly TcpListener _listener;
private readonly List<Connection> _clients;
private readonly object _lock = new object();
bool disposed;
public TcpServer(int port)
{
_listener = new TcpListener(IPAddress.Any, port);
_clients = new List<Connection>();
}
public async Task ListenAsync()
{
try
{
_listener.Start();
Console.WriteLine("Сервер стартовал на " + _listener.LocalEndpoint);
while (true)
{
TcpClient client = await _listener.AcceptTcpClientAsync();
Console.WriteLine("Подключение: " + client.Client.RemoteEndPoint + " > " + client.Client.LocalEndPoint);
lock (_lock)
{
_clients.Add(new Connection(client, RemoveClient));
}
}
}
catch (SocketException)
{
Console.WriteLine("Сервер остановлен.");
}
}
private void RemoveClient(Connection client)
{
lock (_lock)
{
_clients.Remove(client);
client.Dispose();
Console.WriteLine(_clients.Count);
}
}
public void Stop()
{
_listener.Stop();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposed)
throw new ObjectDisposedException(typeof(TcpServer).FullName);
disposed = true;
_listener.Stop();
if (disposing)
{
lock (_lock)
{
Console.WriteLine(_clients.Count);
if (_clients.Count > 0)
{
Console.WriteLine("Отключаю клиентов...");
foreach (Connection client in _clients)
{
client.Dispose();
}
Console.WriteLine("Клиенты отключены.");
}
}
}
}
~TcpServer() => Dispose(false);
}
class Connection : IDisposable
{
private readonly TcpClient _client;
private readonly NetworkStream _stream;
private readonly EndPoint _remoteEndPoint;
private readonly Task _readingTask;
private readonly Task _writingTask;
private readonly Action<Connection> _disposeCallback;
private readonly Channel<string> _channel;
public Connection(TcpClient client, Action<Connection> disposeCallback)
{
_client = client;
_stream = client.GetStream();
_remoteEndPoint = client.Client.RemoteEndPoint;
_disposeCallback = disposeCallback;
_channel = Channel.CreateUnbounded<string>();
_readingTask = RunReadingLoop();
_writingTask = RunWritingLoop();
}
private async Task RunReadingLoop()
{
try
{
byte[] headerBuffer = new byte[4];
while (true)
{
int bytesReceived = await _stream.ReadAsync(headerBuffer, 0, 4);
if (bytesReceived != 4)
break;
int length = BinaryPrimitives.ReadInt32LittleEndian(headerBuffer);
byte[] buffer = new byte[length];
int count = 0;
while (count < length)
{
bytesReceived = await _stream.ReadAsync(buffer, count, buffer.Length - count);
count += bytesReceived;
}
string message = Encoding.UTF8.GetString(buffer);
Console.WriteLine($"<< {_remoteEndPoint}: {message}");
await SendMessageAsync($"Echo: {message}");
}
Console.WriteLine($"Клиент {_remoteEndPoint} отключился.");
_stream.Close();
}
catch (IOException ex)
{
Console.WriteLine($"Подключение к {_remoteEndPoint} закрыто сервером: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine(ex.GetType().Name + ": " + ex.Message);
}
_disposeCallback(this);
}
public async Task SendMessageAsync(string message)
{
Console.WriteLine($">> {_remoteEndPoint}: {message}");
await _channel.Writer.WriteAsync(message);
}
private async Task RunWritingLoop()
{
await foreach (string message in _channel.Reader.ReadAllAsync())
{
byte[] buffer = Encoding.UTF8.GetBytes(message);
byte[] header = new byte[4];
BinaryPrimitives.WriteInt32LittleEndian(header, buffer.Length);
await _stream.WriteAsync(header, 0, header.Length);
await _stream.WriteAsync(buffer, 0, buffer.Length);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
bool disposed;
protected virtual void Dispose(bool disposing)
{
if (disposed)
throw new ObjectDisposedException(typeof(Connection).FullName);
disposed = true;
if (_client.Connected)
{
_channel.Writer.Complete();
_stream.Close();
Task.WaitAll(_readingTask, _writingTask);
}
if (disposing)
{
_client.Dispose();
}
}
~Connection() => Dispose(false);
}
我启动服务器,一切正常。
然后我使用一个非工作客户端,实际上它为此服务器发送 0 作为数据包长度,然后关闭连接。
class Program
{
static void Main(string[] args)
{
var tcpPeer = new TcpPeer("127.0.0.1", 85);
try
{
tcpPeer.Write(Encoding.Unicode.GetBytes("Hello world"));
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
class TcpPeer : IPeer
{
private TcpClient _client;
private NetworkStream _stream;
public TcpPeer(string ip, int port)
{
_client = new TcpClient(ip, port);
_stream = _client.GetStream();
}
public byte[] Read()
{
byte[] data = new byte[8192];
int len = _stream.Read(data, 0, data.Length);
Array.Resize(ref data, len);
return data;
}
public void Write(byte[] msg)
{
//и тут
_stream.Write(msg, 0, msg.Length);
}
}
interface IPeer
{
byte[] Read();
void Write(byte[] data);
}
服务器正确响应关闭连接,然后我给服务器一个命令stop
并抓住ObjectDisposedException
它,这一切都是因为我试图再次关闭已经关闭的连接。在服务器代码中,请注意 2 个位置Console.WriteLine(_clients.Count);
。
这是课堂上的所有问题TcpServer
。神奇之处在于该方法RemoveClient
执行得更早,并且_clients.Count
其中包含 0。但是Dispose
执行得更晚的方法,如果它是 10 秒或更长时间,_clients.Count
则将 1 输出到控制台。
控制台输出。直到“服务器停止”行。结论是正常的,但随后 - 魔术!
Запуск сервера....
Сервер стартовал на 0.0.0.0:85
Слушаем клиента на порту: 85
Подключение: 127.0.0.1:61468 > 127.0.0.1:85
Подключение к 127.0.0.1:61468 закрыто сервером: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host..
0
stop
Остановка сервера...
Сервер остановлен.
1
Отключаю клиентов...
Unhandled exception. System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'ConsoleTcpServer.Connection'.
at ConsoleTcpServer.Connection.Dispose(Boolean disposing) in C:\Source\ConsoleTcpServer\ConsoleTcpServer\Program.cs:line 206
at ConsoleTcpServer.Connection.Dispose() in C:\Source\ConsoleTcpServer\ConsoleTcpServer\Program.cs:line 198
at ConsoleTcpServer.TcpServer.Dispose(Boolean disposing) in C:\Source\ConsoleTcpServer\ConsoleTcpServer\Program.cs:line 110
at ConsoleTcpServer.TcpServer.Dispose() in C:\Source\ConsoleTcpServer\ConsoleTcpServer\Program.cs:line 90
at ConsoleTcpServer.Program.Main(String[] args) in C:\Source\ConsoleTcpServer\ConsoleTcpServer\Program.cs:line 31
at ConsoleTcpServer.Program.<Main>(String[] args)
它与什么有关?为什么列表在一个线程中发生了变化,而在另一个线程中没有?挥发性?但是怎么解释呢?有一种错觉,认为该字段的行为类似于[ThreadStatic]
,但在这种情况下它会是null
,但我有一个列表,它在一个流中只是空的,而在另一个流中则没有。
在调试和发布版本中可重现,无论有无调试器。
我需要一个分布良好且周期长的高质量 HRNG。现存最流行的是梅森漩涡。
我从 C 的作者SFMT 1.5.1那里获取了 SIMD实现, 并用 C# (.NET 6) 重写了它。
抱歉,游戏开发者,这个版本的 Whirlwind 与 Unity 不兼容,不容易修改,我们必须等到 Unity 本身切换到 CoreCLR 和 .NET 6。:(
完成了以下任务:
System.Random
/// <summary>
/// Генератор псевдослучайных чисел на основе алгоритма Вихрь Мерсенна
/// Конфигурация: SFMT-19937:122-18-1-11-1:dfffffef-ddfecb7f-bffaffff-bffffff6
/// </summary>
public class MersenneTwister
{
const int MersenneExponent = 19937;
const int Length128 = MersenneExponent / 128 + 1;
const int Length32 = Length128 * 4;
private readonly Vector128<uint>[] state = new Vector128<uint>[Length128];
private int index;
[ThreadStatic]
private static MersenneTwister _shared;
/// <summary>
/// Общий экземпляр генератора, создается отдельно для каждого потока
/// </summary>
public static MersenneTwister Shared => _shared ??= new(GetRandomSeed());
/// <summary>
/// Создает экземпляр генератора со случайным сидом
/// </summary>
public MersenneTwister()
{
InitGenerator(Shared.NextUInt32());
}
/// <summary>
/// Создает экземпляр генератора с указанным сидом
/// </summary>
/// <param name="seed">Сид для создания генератора</param>
public MersenneTwister(int seed) : this((uint)seed) { }
private MersenneTwister(uint seed)
{
InitGenerator(seed);
}
private static uint GetRandomSeed()
{
ReadOnlySpan<byte> bytes = Guid.NewGuid().ToByteArray();
ReadOnlySpan<uint> hash = MemoryMarshal.Cast<byte, uint>(SHA256.HashData(bytes));
uint result = 0;
foreach (uint value in hash)
result ^= value;
return result;
}
private void InitGenerator(uint seed)
{
if (!Sse2.IsSupported)
throw new InvalidOperationException("SSE2 не поддерживается на данном устройстве.");
Span<uint> values = MemoryMarshal.Cast<Vector128<uint>, uint>(state);
values[0] = seed;
for (int i = 1; i < Length32; i++)
{
values[i] = (uint)(1812433253UL * (values[i - 1] ^ (values[i - 1] >> 30)) + (uint)i);
}
index = Length32;
PeriodCertification(values);
}
private void PeriodCertification(Span<uint> values)
{
uint inner = 0;
ReadOnlySpan<uint> parity = stackalloc uint[] { 0x00000001U, 0x00000000U, 0x00000000U, 0x13c9e684U };
for (int i = 0; i < 4; i++)
inner ^= values[i] & parity[i];
for (int i = 16; i > 0; i >>= 1)
inner ^= inner >> i;
inner &= 1;
if (inner == 1)
return;
for (int i = 0; i < 4; i++)
{
for (uint work = 1; work != 0; work <<= 1)
{
if ((work & parity[i]) != 0)
{
values[i] ^= work;
return;
}
}
}
}
private void UpdateState()
{
const int offset = 122;
int i;
Vector128<uint> r1 = state[^2];
Vector128<uint> r2 = state[^1];
for (i = 0; i < Length128 - offset; i++)
{
state[i] = DoRecursion(state[i], state[i + offset], r1, r2);
r1 = r2;
r2 = state[i];
}
for (; i < Length128; i++)
{
state[i] = DoRecursion(state[i], state[i + offset - Length128], r1, r2);
r1 = r2;
r2 = state[i];
}
}
private Vector128<uint> DoRecursion(Vector128<uint> a, Vector128<uint> b, Vector128<uint> c, Vector128<uint> d)
{
Vector128<uint> z = Sse2.ShiftRightLogical128BitLane(c, 1);
z = Sse2.Xor(z, a);
Vector128<uint> v = Sse2.ShiftLeftLogical(d, 18);
z = Sse2.Xor(z, v);
Vector128<uint> x = Sse2.ShiftLeftLogical128BitLane(a, 1);
z = Sse2.Xor(z, x);
Vector128<uint> y = Sse2.ShiftRightLogical(b, 11);
Vector128<uint> mask = Vector128.Create(0xdfffffefU, 0xddfecb7fU, 0xbffaffffU, 0xbffffff6U);
y = Sse2.And(y, mask);
return Sse2.Xor(z, y);
}
/// <summary>
/// Возвращает число в диапазоне [0,ulong.MaxValue]
/// </summary>
public ulong NextUInt64()
{
if (index >= Length32)
{
UpdateState();
index = 0;
}
Span<ulong> values = MemoryMarshal.Cast<Vector128<uint>, ulong>(state);
ulong r = values[index / 2];
index += 2;
return r;
}
/// <summary>
/// Возвращает число в диапазоне [0,uint.MaxValue]
/// </summary>
public uint NextUInt32()
{
if (index >= Length32)
{
UpdateState();
index = 0;
}
Span<uint> values = MemoryMarshal.Cast<Vector128<uint>, uint>(state);
return values[index++];
}
/// <summary>
/// Возвращает число в диапазоне [0,int.MaxValue)
/// </summary>
public int Next()
{
return (int)(((ulong)int.MaxValue * NextUInt32()) >> 32);
}
/// <summary>
/// Возвращает число в диапазоне [0,maxValue)
/// </summary>
public int Next(int maxValue)
{
if (maxValue <= 0)
throw new ArgumentOutOfRangeException(nameof(maxValue), "Значение должно быть больше 0.");
return (int)(((ulong)maxValue * NextUInt32()) >> 32);
}
/// <summary>
/// Возвращает число в диапазоне [minValue,maxValue)
/// </summary>
public int Next(int minValue, int maxValue)
{
if (minValue < 0)
throw new ArgumentOutOfRangeException(nameof(minValue), "Значение должно быть больше либо равно 0.");
if (maxValue <= minValue)
throw new ArgumentOutOfRangeException(nameof(maxValue), $"Значение должно быть больше, чем {nameof(minValue)}.");
return (int)(((ulong)(maxValue - minValue) * NextUInt32()) >> 32) + minValue;
}
/// <summary>
/// Возвращает число в диапазоне [0,long.MaxValue)
/// </summary>
public long NextInt64()
{
return (long)(((BigInteger)long.MaxValue * NextUInt64()) >> 64);
}
/// <summary>
/// Возвращает число в диапазоне [0,maxValue)
/// </summary>
public long NextInt64(long maxValue)
{
if (maxValue <= 0)
throw new ArgumentOutOfRangeException(nameof(maxValue), "Значение должно быть больше 0.");
return (long)(((BigInteger)maxValue * NextUInt64()) >> 64);
}
/// <summary>
/// Возвращает число в диапазоне [minValue,maxValue)
/// </summary>
public long NextInt64(long minValue, long maxValue)
{
if (minValue < 0)
throw new ArgumentOutOfRangeException(nameof(minValue), "Значение должно быть больше либо равно 0.");
if (maxValue <= minValue)
throw new ArgumentOutOfRangeException(nameof(maxValue), $"Значение должно быть больше, чем {nameof(minValue)}.");
return (long)(((BigInteger)(maxValue - minValue) * NextUInt64()) >> 64) + minValue;
}
/// <summary>
/// Возвращает число в диапазоне [0,1)
/// </summary>
public double NextDouble()
{
return (NextUInt64() >> 11) / 9007199254740992.0; // 2^53
}
/// <summary>
/// Возвращает число в диапазоне [0,1)
/// </summary>
public float NextSingle()
{
return (NextUInt32() >> 8) / 16777216.0f; // 2^24
}
}
它很容易使用,如下所示:
static void Main(string[] args)
{
MersenneTwister mt = new MersenneTwister();
int randomNumber = mt.Next(10);
Console.WriteLine(randomNumber);
Console.ReadKey();
}
性能方面,不错,我就不给数字System.Random
了.
它在同一个种子上产生相同的数字序列,就像SFMT 1.5.1在 C 中的原始开发一样,也就是说,这里的计算也一切正常。
从System.Random
没有开始被继承,因为不想拉它的内部逻辑。而且由于他自己System.Random
没有实现任何东西(他们可能已经为它附加了这么多年的接口),我没有看到联系他的意义。
请看看我在公共方法的转换中是否一切正常,以及我是否对种子太聪明了。或者可能需要调整以使其更快?
这个问题可能很难客观地回答,但对我来说很相关,我会尽量具体化,读到最后。
我同时进行 Web 开发(前端)和桌面开发(.NET)。我有大型项目的经验,主要是作为产品/客户所有者。
一个大型项目从编写基本文档开始,大致分为以下步骤:
总的来说,要开始写任何东西,需要大量的工作,最好是一个十人的团队,至少需要 2 周。
通常对于单身人士来说是这样的——“让我们屏蔽一些界面,在代码中写下每个元素的行为,然后你就完成了。” 理解你要做什么和你在代码中写什么的过程同时发生,甚至代码跑在理解之前,结果是“随机开发”,然后要么是客户大吃一惊的眼睛”小心你想要的”,或者理解“一切都需要重写”。
也就是说,我看到了两个极端:完整的开发周期和生产中的错误。对于需要将项目从订单到发布的孤独的开发人员来说,中庸之道在哪里?问题总是一样的:在尽可能短的时间内开发人员的最佳效率。
问题如下:比如我一个人,比如有一个具体的口头任务“写另一个Telegram客户端”并且有对栈的理解,比如“.NET WPF + MVVM”,虽然这个在这个问题的背景下并不重要。然后什么?专业人士是怎么做的?
之前,我已经写了一个类似问题的答案:How to ensure code execution in 1 thread after await c#.net
但是需要在应用程序中重用单线程同步的魅力,而无需使用异步代码进行任何阻塞。众所周知,lock
其他同步原语不能在异步代码中使用。好吧,除了信号量,但信号量并不是很原始。
要重现问题,您甚至不需要集合,您可以运行以下代码:
static async Task Main(string[] args)
{
await AddAsync(1000);
Console.WriteLine(_counter);
}
static int _counter;
static async Task AddAsync(int count)
{
Task[] tasks = new Task[count];
for (int i = 0; i < count; i++)
tasks[i] = IncrementAsync();
await Task.WhenAll(tasks);
}
static async Task IncrementAsync()
{
await Task.Delay(1);
_counter++;
}
并在控制台中获取例如此输出
969
它应该是1000
。这意味着代码不是线程安全的。如果您从 WPF 或 Winforms 中的 UI 线程运行此代码,它将 100% 正常工作,并且始终返回1000
. 在通过引用提出的解决方案中,上下文本身的启动看起来非常麻烦,并且对于每次启动,您都需要创建一个新线程或将当前线程提供给上下文。
我想找到一个更简单的解决方案,以便您可以将多线程代码的不同部分中的调用重定向到您自己在特定线程中运行的某种上下文。
我坐下来思考如何摆脱重复的代码。
2048你知道这个游戏,我正在编写我自己的实现版本。有一个瓷砖数组,每个瓷砖是
const cell = {
row : 1,
col : 1,
value : 32,
merged : false,
node : <HTMLElement>
}
从逻辑上讲,有一个 4x4 瓷砖的运动场。
任务:当玩家移动时,你需要穿过整个场地,并沿着正确的方向移动瓷砖,将具有相同价值的瓷砖合并。
这段代码有效,但看起来很糟糕。它仅使用一组瓦片来实现移动和合并瓦片的逻辑。从技术上讲,DOM 是用另一种方法更新的,瓦片的实际合并也发生在后面,这里只在瓦片上放了一个标签,它将被合并。要删除的图块只需设置为 0。
const size = 4;
function move(direction) {
let moved = false;
if (direction === 'left') {
for (let col = 1; col < size; col++) {
for (let row = 0; row < size; row++) {
const cell = getCell(row, col);
if (cell && !cell.merged) {
let targetCol;
for (targetCol = col - 1; targetCol >= 0; targetCol--) {
const targetCell = getCell(row, targetCol);
if (!targetCell)
continue;
if (targetCell.value == cell.value && !targetCell.merged) {
cell.value = 0;
targetCell.merged = true;
}
break;
}
if (cell.value != 0)
targetCol++;
if (col != targetCol) {
moveCol(cell, targetCol);
moved = true;
}
}
}
}
}
if (direction === 'right') {
for (let col = size - 2; col >= 0; col--) {
for (let row = 0; row < size; row++) {
const cell = getCell(row, col);
if (cell && !cell.merged) {
let targetCol;
for (targetCol = col + 1; targetCol < size; targetCol++) {
const targetCell = getCell(row, targetCol);
if (!targetCell)
continue;
if (targetCell.value == cell.value && !targetCell.merged) {
cell.value = 0;
targetCell.merged = true;
}
break;
}
if (cell.value != 0)
targetCol--;
if (col != targetCol) {
moveCol(cell, targetCol);
moved = true;
}
}
}
}
}
if (direction === 'up') {
for (let row = 1; row < size; row++) {
for (let col = 0; col < size; col++) {
const cell = getCell(row, col);
if (cell && !cell.merged) {
let targetRow;
for (targetRow = row - 1; targetRow >= 0; targetRow--) {
const targetCell = getCell(targetRow, col);
if (!targetCell)
continue;
if (targetCell.value == cell.value && !targetCell.merged) {
cell.value = 0;
targetCell.merged = true;
}
break;
}
if (cell.value != 0)
targetRow++;
if (row != targetRow) {
moveRow(cell, targetRow);
moved = true;
}
}
}
}
}
if (direction === 'down') {
for (let row = size - 2; row >= 0; row--) {
for (let col = 0; col < size; col++) {
const cell = getCell(row, col);
if (cell && !cell.merged) {
let targetRow;
for (targetRow = row + 1; targetRow < size; targetRow++) {
const targetCell = getCell(targetRow, col);
if (!targetCell)
continue;
if (targetCell.value == cell.value && !targetCell.merged) {
cell.value = 0;
targetCell.merged = true;
}
break;
}
if (cell.value != 0)
targetRow--;
if (row != targetRow) {
moveRow(cell, targetRow);
moved = true;
}
}
}
}
}
return moved;
}
我找不到优化额头的方法。也许你需要改变算法,帮我弄清楚。
算法,例如,用于向左(左)移动:
上面代码中调用的方法,供参考
const cells = [];
const cellSize = 100 / size;
function moveRow(cell, row) {
cell.row = row;
cell.node.style.top = (row * cellSize) + '%';
}
function moveCol(cell, col) {
cell.col = col;
cell.node.style.left = (col * cellSize) + '%';
}
function getCell(row, col) {
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
if (cell.row == row && cell.col == col && cell.value != 0)
return cell;
}
}
我正在使用Microsoft的光线追踪软件实现示例。其实这个例子是关于多线程的,但是吸引我的是3D渲染算法。
这是方法。如果是尾递归,我可以很容易地做到这一点,但我还不能在这里做到。
using System.Numerics;
private const int maxDepth = 6;
private Vector3 TraceRay(Ray ray, Scene scene, int depth)
{
(SceneObject obj, float distance) = ClosestIntersection(ray, scene);
if (obj is null)
return Vector3.Zero;
Vector3 pos = distance * ray.Direction + ray.Start;
Vector3 normal = obj.Normal(pos);
Vector3 reflectDir = Vector3.Reflect(ray.Direction, normal);
Vector3 color = GetNaturalColor(obj, pos, normal, reflectDir, scene);
if (depth >= maxDepth)
return color;
// типы = float Vector3
return color + obj.Surface.Reflect(pos) * obj.Surface.Diffuse(pos) * TraceRay(new Ray(pos, reflectDir), scene, depth + 1);
}
初始调用:
Vector3 color = TraceRay(new Ray(camera.Pos, GetPoint(x, y, camera)), scene, 0);
是否可以在不分配额外内存的情况下进行线性优化?或者有一个选择,但这样它至少会更快一点。
我不会展示从这里调用的方法和我的数据结构,它们不会影响问题的本质。但如果你需要 - 写,我会补充这个问题。
这里是光束。
readonly ref struct Ray
{
public readonly Vector3 Start;
public readonly Vector3 Direction;
public Ray(Vector3 start, Vector3 direction)
{
Start = start;
Direction = direction;
}
}
顺便说一句,我推导出这样的像素颜色公式
c0 + r0 * (c1 + r1 * (c2 + r2 * (c3 + r3 * (c4 + r4 * (c5 + r5 * (c6))))))
cN
- 与照明相关的物体颜色
rN
- 反射系数
N
- 迭代
我经常遇到应用程序,包括来自 Microsoft 的应用程序,它们的开发人员懒得告诉用户真正发生了什么以及应用程序崩溃的原因。
例子:
这些不是断章取义的文本,而是您一生中至少见过一次的完整消息,除了您仍然可以在末尾添加“...稍后再试”。仅此而已。
这里还有一些问题,错误处理的一切都非常糟糕。不是技术上的,而是逻辑上的。可以给这篇文章的链接以帮助新人。
例如,为了简单起见,让我们以一个写入文件的控制台应用程序为例。
// плохой код
static void Main(string[] args)
{
Console.Write("Введите имя файла для записи: ");
string path = Console.ReadLine();
try
{
File.WriteAllText(path, "Hello World!");
Console.WriteLine("Файл записан!");
}
catch
{
Console.WriteLine("Что-то пошло не так!");
}
}
可以根据您的判断来描述答案:
一般来说,我们需要一个问题的答案:桌面应用程序的新手开发人员应该如何处理异常,以便用户在异常发生时不会感到困惑?
(不会有自我反应,我也对这个不好)
有各种反编译异步代码的示例展示了异步状态机的内部。但它看起来很复杂,目前还不清楚它是什么,以及它是如何工作的。
是否可以以一个简单的异步方法为例,在行为上编写完全相同,但没有async/await
? 要使用调试器,请逐步了解该方法以了解其工作原理。
比如这个:
private static async Task GoAsync()
{
for (int i = 1; i <= 10; i++)
{
await Task.Delay(200);
Console.WriteLine(i);
}
for (int i = 10; i > 0; i--)
{
await Task.Delay(200);
Console.WriteLine(i);
}
}
该方法只计算从 1 到 10 和从 10 到 1 的延迟。
发射:
static async Task Main(string[] args)
{
await GoAsync();
Console.WriteLine("Done.");
Console.ReadKey();
}
我最近重构了一个项目,创建了泛型方法,在参数中抽象而不是具体类型,并注意到应用程序的性能略有下降。我开始挖掘,发现循环是一切的罪魁祸首foreach
。
似乎foreach
针对数组进行了优化。决定试一试。
这里有2个完全相同的方法,原始数据是同一个数组。区别仅在于签名。
class Program
{
static void Main(string[] args)
{
var result = BenchmarkRunner.Run<ForeachBenchmarks>();
Console.ReadKey();
}
}
[MemoryDiagnoser]
public class ForeachBenchmarks
{
private readonly int[] _numbers = Enumerable.Repeat(1, 1000000).ToArray();
public IEnumerable<int[]> Numbers { get { yield return _numbers; } }
[Benchmark]
[ArgumentsSource(nameof(Numbers))]
public int SumArray(int[] numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
return sum;
}
[Benchmark]
[ArgumentsSource(nameof(Numbers))]
public int SumIEnumerable(IEnumerable<int> numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
return sum;
}
}
确实优化了:
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.1081 (21H1/May2021Update)
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET SDK=5.0.301
[Host] : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
DefaultJob : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
方法 | 数字 | 意思是 | 错误 | 标准差 | 0代 | 第一代 | 第 2 代 | 已分配 |
---|---|---|---|---|---|---|---|---|
和数组 | int32[1000000] | 468.9 我们 | 1.84 我们 | 1.44 我们 | - | - | - | - |
SumIEnumerable | int32[1000000] | 5,808.0 我们 | 44.16 我们 | 39.15 我们 | - | - | - | 32B |
请解释为什么使用数组foreach
的接口比使用接口慢 10 倍?IEnumerable<T>
T[]
UPD:我测试了List<int>
和ReadOnlySpan<int>
。对于 list foreach
,性能与 for 相同,IEnumerable<T>
对于 span ,与数组相同。
相当简单的任务 - 有 N 个有序请求必须异步执行,同时执行的请求数量有限制,然后从它们中取出它们以及 N 个有序响应。
实际上,当有很多请求时,这可以与网络或数据库一起使用,但您不想为您的计算机、网络或服务器安排灾难。因此,对同时活动的异步任务的数量进行了限制。
正面的解决方案看起来很明显。
public async Task<Tout[]> RunSemaphoreAsync<Tin, Tout>(IEnumerable<Tin> items, Func<Tin, Task<Tout>> func, int degree)
{
using SemaphoreSlim semaphore = new(degree);
return await Task.WhenAll(items.Select(async item => {
await semaphore.WaitAsync();
try
{
return await func(item);
}
finally
{
semaphore.Release();
}
}));
}
使用示例:
让它成为一项接受某事、做某事并回馈某事的工作任务。
public async Task<int> RunJobAsync(int n)
{
await Task.Yield();
return n + 1;
}
这是一个例子。
IEnumerable<int> numbers = Enumerable.Range(0, 100);
int[] result = await RunSemaphoreAsync(numbers, RunJobAsync, Environment.ProcessorCount * 2);
Console.WriteLine(string.Join(",", result));
一切都像发条一样,快速且按预期工作。
上面的一切都很好,但是有一天,当我再次阅读生产者/消费者模式的各种实现时,我想到了使用工人而不是信号量的想法。其实,为什么不呢。
这是方法。
public async Task<Tout[]> RunWorkersAsync<Tin, Tout>(IEnumerable<Tin> items, Func<Tin, Task<Tout>> func, int degree)
{
List<Task<Tout>> tasks = new();
using (var source = items.GetEnumerator())
{
Task[] jobs = new Task[degree];
for (int i = 0; i < degree; i++)
{
jobs[i] = ((Func<Task>)(async () =>
{
while (true)
{
Task<Tout> task;
lock (source)
{
if (source.MoveNext())
{
task = func(source.Current);
tasks.Add(task);
}
else
break;
}
await task;
}
}))();
}
await Task.WhenAll(jobs);
}
return tasks.Select(t => t.Result).ToArray();
}
和第一个候选人一样漂亮。那么什么更好呢?
我决定测量开销。
我不是编写基准测试的高手,但这什么时候阻止了任何人?:)
class Program
{
static void Main(string[] args)
{
var result = BenchmarkRunner.Run<MyBenchmarks>();
Console.ReadKey();
}
}
[MemoryDiagnoser]
public class MyBenchmarks
{
private readonly List<int> numbers = Enumerable.Range(0, 2000).ToList();
private readonly int degree = Environment.ProcessorCount * 2;
[Benchmark]
public Task SemaphoreTest()
{
return RunSemaphoreAsync(numbers, RunJobAsync, degree);
}
[Benchmark]
public Task WorkersTest()
{
return RunWorkersAsync(numbers, RunJobAsync, degree);
}
public async Task<int> RunJobAsync(int n)
{
await Task.Yield();
return n + 1;
}
public async Task<Tout[]> RunSemaphoreAsync<Tin, Tout>(IEnumerable<Tin> items, Func<Tin, Task<Tout>> func, int degree)
{
using SemaphoreSlim semaphore = new(degree);
return await Task.WhenAll(items.Select(async item => {
await semaphore.WaitAsync();
try
{
return await func(item);
}
finally
{
semaphore.Release();
}
}));
}
public async Task<Tout[]> RunWorkersAsync<Tin, Tout>(IEnumerable<Tin> items, Func<Tin, Task<Tout>> func, int degree)
{
List<Task<Tout>> tasks = new();
using (var source = items.GetEnumerator())
{
Task[] jobs = new Task[degree];
for (int i = 0; i < degree; i++)
{
jobs[i] = ((Func<Task>)(async () =>
{
while (true)
{
Task<Tout> task;
lock (source)
{
if (source.MoveNext())
{
task = func(source.Current);
tasks.Add(task);
}
else
break;
}
await task;
}
}))();
}
await Task.WhenAll(jobs);
}
return tasks.Select(t => t.Result).ToArray();
}
}
在这里我得到了一个有趣的结果
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.1081 (21H1/May2021Update)
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET SDK=5.0.301
[Host] : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
DefaultJob : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
方法 | 意思是 | 错误 | 标准差 | 0代 | 第一代 | 第 2 代 | 已分配 |
---|---|---|---|---|---|---|---|
信号量测试 | 1,780.2 我们 | 4.23 我们 | 3.95 我们 | 140.6250 | 41.0156 | - | 519KB |
工人测试 | 943.2us | 18.37 我们 | 26.92 我们 | 74.2188 | 19.5313 | - | 262KB |
眼睛拒绝相信。为什么带有信号量的方法只是像这样合并,或者可能是一个歪曲的测试或歪曲的实现?请考虑。
PS我没有立即在SO上运行,但首先我寻找关于缓慢的投诉SemaphoreSlim
......并没有找到它,但我发现了这个:https ://github.com/dotnet/runtime/pull/55262 。换句话说,红绿灯在 .NET 6 中会飞一点。
网上有很多关于如何在应用程序中显示当前时间的解决方案。有定时器和异步的解决方案。
但我发现的所有解决方案都存在一个问题——它们导致界面并非每秒更新一次。
10:33:12 => 10:33:14
。看起来任务看似简单,但我没有找到现成的解决方案,或者我看起来很糟糕。我决定自己写。
有一项任务 - 组织搜索不同语言的单词文本。同时,所有可能的 Unicode 的变音符号和其他乐趣都可以出现在单词中。
例如,有 2 艘船的名称 - GermanGroßer Kurfürst
和 Polish Błyskawica
。我想在文本中写下grosser
或blyska
找到这些标题。为了让这一切成为可能,我们使用了文本规范化功能,该功能既适用于搜索字符串,也适用于文本本身。她将被进一步讨论。
有一个有效的、经过验证的、防弹的解决方案。
public static string NormalizeUnicode(string text)
{
string stFormD = text.ToLower()
.Replace('\u00A0', ' ') // неразрывный пробел
.Replace("ß", "ss")
.Replace('ł', 'l')
.Normalize(NormalizationForm.FormD);
StringBuilder sb = new StringBuilder(stFormD.Length);
foreach (char c in stFormD)
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
sb.Append(c);
return sb.ToString().Normalize(NormalizationForm.FormC);
}
输出将分别给出grosser kurfurst
和blyskawica
。唯一的问题是它甚至可以以可接受的速度工作,但我希望它更快。
自从使用该解决方案(在英文 Internet 上找到)以来,已经过去了很长时间,在此期间,.NET 5 出现了它的跨度和其他 C# 8-9 的乐趣。我试图优化。
检查代码
public static string NormalizeUnicodeNew(string text)
{
ReadOnlySpan<char> stFormD = text.Normalize(NormalizationForm.FormD);
StringBuilder sb = new StringBuilder(stFormD.Length);
foreach (char c in stFormD)
{
switch (CharUnicodeInfo.GetUnicodeCategory(c))
{
case UnicodeCategory.UppercaseLetter:
sb.Append(char.ToLower(c));
break;
case not UnicodeCategory.NonSpacingMark:
if (c == 'ß')
sb.Append("ss");
else
sb.Append(c switch
{
'\u00A0' => ' ',
'ł' => 'l',
_ => c
});
break;
}
}
return sb.ToString().Normalize(NormalizationForm.FormC);
}
测试代码
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
string s = "Großer Kurfürst, Błyskawica";
string text = string.Concat(Enumerable.Repeat(s, 1000000));
Console.WriteLine(s);
Console.WriteLine(NormalizeUnicode(s));
Stopwatch sw = new Stopwatch();
sw.Start();
string n = NormalizeUnicode(text);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
sw.Restart();
string m = NormalizeUnicodeNew(text);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(m == n);
Console.ReadKey();
}
发布版本的控制台输出
Großer Kurfürst, Błyskawica
grosser kurfurst, blyskawica
780
562
True
速度的提升真的很明显~15%,这已经是胜利了。我粗略地测量了时间,我还没有运行基准测试。
我想知道你的意见和建议,我还能在哪里调整。有一个想法可以使用string.Create
,但它崩溃了,因为我不知道过滤后输出字符串会持续多长时间。
我了解到,在使用内部时,System.Runtime.Intrinsics.X86
不必使用指针来寻址数据,但您可以简单地使用 转换一个数据数组,它的工作速度与在代码System.Runtime.InteropServices.MemoryMarshal
中使用指针一样快。unsafe
我很惊讶并使用Benchmark.NET测试了性能。
我写了 4 个基准,一个标量来检查结果,用它来检查System.Numerics.Vector<T>
性能(这很有趣),实际上是 2 个基于Vector256<int>
托管和非托管代码的测试。
我做了最简单的任务——1000 万个元素的数组元素的总和。我意识到该实现有一个限制,数组的长度必须是 8 的倍数 - 256 位向量的长度(8 x 32),否则输出中的结果将是不可预测的。
public class SumTest
{
private static readonly int[] _numbers = Enumerable.Repeat(2, 100000000).ToArray();
public IEnumerable<object> Params
{
get
{
yield return _numbers;
}
}
[Benchmark]
[ArgumentsSource(nameof(Params))]
public int SumScalar(int[] numbers)
{
int result = 0;
for (int i = 0; i < numbers.Length; i++)
{
result += numbers[i];
}
return result;
}
[Benchmark]
[ArgumentsSource(nameof(Params))]
public int SumNumerics(int[] numbers)
{
Vector<int> acc = Vector<int>.Zero;
for (int i = 0; i < numbers.Length; i += Vector<int>.Count)
{
Vector<int> v = new Vector<int>(numbers, i);
acc += v;
}
return Vector.Dot(acc, Vector<int>.One);
}
[Benchmark]
[ArgumentsSource(nameof(Params))]
public int SumIntrinsics(int[] numbers)
{
ReadOnlySpan<Vector256<int>> vectors = MemoryMarshal.Cast<int, Vector256<int>>(numbers);
Vector256<int> acc = Vector256<int>.Zero;
for (int i = 0; i < vectors.Length; i++)
{
acc = Avx2.Add(acc, vectors[i]);
}
Vector128<int> r = Ssse3.HorizontalAdd(acc.GetUpper(), acc.GetLower());
r = Ssse3.HorizontalAdd(r, r);
r = Ssse3.HorizontalAdd(r, r);
return r.GetElement(0);
}
[Benchmark]
[ArgumentsSource(nameof(Params))]
public unsafe int SumIntrinsicsUnsafe(int[] numbers)
{
Vector256<int> acc = Vector256<int>.Zero;
fixed (int* numPtr = numbers)
{
int* endPtr = numPtr + numbers.Length;
for (int* numPos = numPtr; numPos < endPtr; numPos += 8)
{
Vector256<int> v = Avx.LoadVector256(numPos);
acc = Avx2.Add(acc, v);
}
Vector128<int> r = Ssse3.HorizontalAdd(acc.GetUpper(), acc.GetLower());
r = Ssse3.HorizontalAdd(r, r);
r = Ssse3.HorizontalAdd(r, r);
return r.GetElement(0);
}
}
}
检查了输出
int[] numbers = Enumerable.Repeat(2, 100000000).ToArray();
SumTest sum = new SumTest();
Console.WriteLine(sum.SumScalar(numbers));
Console.WriteLine(sum.SumNumerics(numbers));
Console.WriteLine(sum.SumIntrinsics(numbers));
Console.WriteLine(sum.SumIntrinsicsUnsafe(numbers));
200000000
200000000
200000000
200000000
也就是说,一切正常。
我收集并启动了基准测试。
var summary = BenchmarkRunner.Run<SumTest>();
而他又吃了一惊。
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.102
[Host] : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT
DefaultJob : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT
| Method | numbers | Mean | Error | StdDev |
|-------------------- |----------------- |---------:|---------:|---------:|
| SumScalar | Int32[100000000] | 83.69 ms | 0.466 ms | 0.436 ms |
| SumNumerics | Int32[100000000] | 31.30 ms | 0.303 ms | 0.268 ms |
| SumIntrinsics | Int32[100000000] | 28.98 ms | 0.282 ms | 0.236 ms |
| SumIntrinsicsUnsafe | Int32[100000000] | 28.80 ms | 0.191 ms | 0.169 ms |
也就是说,统计误差之间SumIntrinsics
和SumIntrinsicsUnsafe
内部的差异(StdDev
)。
问题:这是什么动物MemoryMarshal
,现在在使用内在函数时使用它是否有意义unsafe
,实际上是使用向量?
如果问题是是否可以将计算结果以安全代码写入数组 - 是的,有可能,就像数组被强制转换并且写入向量的所有信息都将在数组中一样,即也就是说,工作与使用常规结构数组完全相同。换句话说,不安全代码的好处并不是立即可见的。好吧,仅当源数据最初以指针的形式出现,而不是以托管数组的形式出现时,但那里可能存在细微差别,我对此主题并不深入。
顺便说一句,我很惊喜Vector<T>
。我认为在代码对性能不是超级敏感的情况下,可以使用它来Numerics
支持跨处理器。
添加
我尝试再次重写该方法SumNumerics
或添加另一个版本的实现SumIntrinsicsHybrid
。
[Benchmark]
[ArgumentsSource(nameof(Params))]
public int SumNumerics(int[] numbers)
{
ReadOnlySpan<Vector<int>> vectors = MemoryMarshal.Cast<int, Vector<int>>(numbers);
Vector<int> acc = Vector<int>.Zero;
for (int i = 0; i < vectors.Length; i ++)
{
acc += vectors[i];
}
return Vector.Dot(acc, Vector<int>.One);
}
[Benchmark]
[ArgumentsSource(nameof(Params))]
public unsafe int SumIntrinsicsHybrid(int[] numbers)
{
ReadOnlySpan<Vector256<int>> vectors = MemoryMarshal.Cast<int, Vector256<int>>(numbers);
Vector256<int> acc = Vector256<int>.Zero;
fixed (Vector256<int>* numPtr = vectors)
{
Vector256<int>* endPtr = numPtr + vectors.Length;
for (Vector256<int>* numPos = numPtr; numPos < endPtr; numPos++)
{
acc = Avx2.Add(acc, *numPos);
}
Vector128<int> r = Ssse3.HorizontalAdd(acc.GetUpper(), acc.GetLower());
r = Ssse3.HorizontalAdd(r, r);
r = Ssse3.HorizontalAdd(r, r);
return r.GetElement(0);
}
}
基准再次表明,在帮助下进行铸造MemoryMarshal
,如果不是免费的,那么完全可以得到回报。
| Method | numbers | Mean | Error | StdDev |
|-------------------- |----------------- |---------:|---------:|---------:|
| SumScalar | Int32[100000000] | 83.30 ms | 0.214 ms | 0.189 ms |
| SumNumerics | Int32[100000000] | 28.85 ms | 0.222 ms | 0.207 ms |
| SumIntrinsics | Int32[100000000] | 28.74 ms | 0.145 ms | 0.136 ms |
| SumIntrinsicsUnsafe | Int32[100000000] | 28.14 ms | 0.234 ms | 0.195 ms |
| SumIntrinsicsHybrid | Int32[100000000] | 28.09 ms | 0.174 ms | 0.163 ms |
小阵列测试
| Method | numbers | Mean | Error | StdDev |
|-------------------- |------------ |----------:|---------:|---------:|
| SumScalar | Int32[1000] | 712.65 ns | 2.889 ns | 2.702 ns |
| SumNumerics | Int32[1000] | 81.22 ns | 0.466 ns | 0.436 ns |
| SumIntrinsics | Int32[1000] | 82.63 ns | 0.311 ns | 0.291 ns |
| SumIntrinsicsUnsafe | Int32[1000] | 60.66 ns | 0.347 ns | 0.308 ns |
| SumIntrinsicsHybrid | Int32[1000] | 61.01 ns | 0.418 ns | 0.370 ns |
我最终决定学习如何在 MVVM 框架内使应用程序界面可扩展为多个 View / ViewModel。我遇到了Inversion of Control + Depency Injection 模板,上面有很多信息,很多人建议用它来组织应用程序的逻辑架构。此外,它还提供了组织诸如单例存储等便利的机会,这正是我所需要的。
问题是,在作者推荐的文章中,这就是全部,几乎没有人在 WPF + MVVM 中“为傻瓜”手动显示 IoC 实现。
如果不了解它是如何工作的,我就不能在我的项目中使用“黑匣子”,我通常不喜欢大的、可怕的和无法理解的东西,不清楚它是如何工作的,我总是试图挖掘这个非常本质。我决定对一项实际任务进行分析 - 制作带有ListBox
页面选择的多页面界面。
从两个接口开始
public interface IView
{
string Title { get; set; }
object DataContext { get; set; }
}
public interface IViewModel
{
}
然后把他们都搞砸了
public sealed partial class MainWindow : Window, IView {...}
public partial class Page1 : Page, IView {...}
public partial class Page2 : Page, IView {...}
public class MainViewModel : NotifyPropertyChanged, IViewModel {...}
public class Page1ViewModel : NotifyPropertyChanged, IViewModel {...}
public class Page2ViewModel : NotifyPropertyChanged, IViewModel {...}
我用单例实现了这两个容器
public static class ViewService
{
private static readonly ConcurrentDictionary<Type, IView> instances = new ConcurrentDictionary<Type, IView>();
public static T GetView<T>(IViewModel viewModel = null) where T : IView
{
if (!instances.TryGetValue(typeof(T), out IView view))
{
view = (T)Activator.CreateInstance(typeof(T));
instances[typeof(T)] = view;
}
if (viewModel != null)
view.DataContext = viewModel;
return (T)view;
}
}
public static class ViewModelService
{
private static readonly ConcurrentDictionary<Type, IViewModel> instances = new ConcurrentDictionary<Type, IViewModel>();
public static T GetViewModel<T>() where T : IViewModel
{
if (!instances.TryGetValue(typeof(T), out IViewModel viewModel))
{
viewModel = (T)Activator.CreateInstance(typeof(T));
instances[typeof(T)] = viewModel;
}
return (T)viewModel;
}
}
我像这样创建主窗口
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainViewModel vm = ViewModelService.GetViewModel<MainViewModel>();
MainWindow window = ViewService.GetView<MainWindow>(vm);
window.Show();
}
}
并且页面在主视图模型的构造函数中
public class MainViewModel : NotifyPropertyChanged, IViewModel
{
private IView _currentPage;
public List<IView> Pages { get; }
public IView CurrentPage
{
get => _currentPage;
set
{
_currentPage = value;
OnPropertyChanged();
OnPropertyChanged(nameof(Title));
}
}
public string Title => $"IoC Demo - {CurrentPage.Title}";
public MainViewModel()
{
Pages = new List<IView>
{
ViewService.GetView<Page1>(ViewModelService.GetViewModel<Page1ViewModel>()),
ViewService.GetView<Page2>(ViewModelService.GetViewModel<Page2ViewModel>())
};
CurrentPage = Pages[0];
}
}
主窗口MainWindow
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox BorderThickness="0" Background="AliceBlue" ItemsSource="{Binding Pages}" SelectedItem="{Binding CurrentPage}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Frame Content="{Binding CurrentPage}" Grid.Column="1" NavigationUIVisibility="Hidden"/>
</Grid>
页面是一样的,只有一种类型Page1
,第二种Page2
<Grid>
<TextBlock Margin="5" Text="{Binding Text}"/>
</Grid>
页面视图模型(用于可见性)
public class Page1ViewModel : NotifyPropertyChanged, IViewModel
{
public string Text { get; } = "Page one";
}
这一切都可以协同工作。
问题:我是否违反了 MVVM,我是否正确地使用单例实现了 IoC 容器?
附言
我一直想知道如何让HttpClient
cookies像浏览器一样工作,然后在退出应用程序时保存,重启后继续使用。然后,最后,它找到了我,我做到了。
让它HttpClient
像这样工作:
检查代码
public static class HttpManager
{
private static readonly HttpClientHandler handler = new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
AllowAutoRedirect = true
};
private static readonly HttpClient client = new HttpClient(handler)
{
DefaultRequestVersion = new Version(2, 0)
};
static HttpManager()
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0");
client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
}
// GET
public static async Task<string> GetPageAsync(string url)
{
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
return await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
}
// POST
public static async Task<string> PostFormAsync(string url, Dictionary<string, string> data)
{
using HttpContent content = new FormUrlEncodedContent(data);
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
return await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
}
// Загрузить и расшифровать Cookie из файла
public static async Task LoadCookiesAsync(string filename)
{
if (File.Exists(filename))
{
using FileStream fs = File.OpenRead(filename);
byte[] IV = new byte[16];
fs.Read(IV);
byte[] protectedKey = new byte[178];
fs.Read(protectedKey);
using AesManaged aes = new AesManaged
{
Key = ProtectedData.Unprotect(protectedKey, IV, DataProtectionScope.CurrentUser),
IV = IV
};
using CryptoStream cs = new CryptoStream(fs, aes.CreateDecryptor(), CryptoStreamMode.Read, true);
CookieCollection cookies = await JsonSerializer.DeserializeAsync<CookieCollection>(cs);
foreach (Cookie cookie in cookies)
{
// не загружать, если кука заэкспайрилась
if (!cookie.Expired && (cookie.Expires == DateTime.MinValue || cookie.Expires > DateTime.Now))
handler.CookieContainer.Add(cookie);
}
}
}
// Зашифровать и сохранить Cookie в файл
public static async Task SaveCookiesAsync(string filename)
{
using AesManaged aes = new AesManaged();
using FileStream fs = File.Create(filename);
fs.Write(aes.IV);
fs.Write(ProtectedData.Protect(aes.Key, aes.IV, DataProtectionScope.CurrentUser));
using CryptoStream cs = new CryptoStream(fs, aes.CreateEncryptor(), CryptoStreamMode.Write, true);
await JsonSerializer.SerializeAsync(cs, handler.CookieContainer.GetAllCookies());
}
}
public static class CookieContainerExtensions
{
// Забирает все куки из контейнера
public static CookieCollection GetAllCookies(this CookieContainer container)
{
CookieCollection allCookies = new CookieCollection();
IDictionary domains = (IDictionary)container.GetType()
.GetRuntimeFields()
.FirstOrDefault(x => x.Name == "m_domainTable")
.GetValue(container);
foreach (object field in domains.Values)
{
IDictionary values = (IDictionary)field.GetType()
.GetRuntimeFields()
.FirstOrDefault(x => x.Name == "m_list")
.GetValue(field);
foreach (CookieCollection cookies in values.Values)
{
allCookies.Add(cookies);
}
}
return allCookies;
}
}
Cookie 被序列化为 Json,通过 DPAPI 使用密钥保护加密,并保存到磁盘。
使用已检查代码的可重现示例
使用登录名和密码对 StackOverflow 进行授权
using HtmlAgilityPack;
using Fizzler.Systems.HtmlAgilityPack;
private const string filename = "cookies.bin";
static async Task Main(string[] args)
{
await HttpManager.LoadCookiesAsync(filename);
string html = await HttpManager.GetPageAsync("https://ru.stackoverflow.com/");
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(html);
if (doc.DocumentNode.QuerySelector(".top-bar ol").HasClass("user-logged-out"))
{
html = await HttpManager.GetPageAsync("https://ru.stackoverflow.com/users/login?ssrc=head&returnurl=https%3a%2f%2fru.stackoverflow.com%2f");
doc = new HtmlDocument();
doc.LoadHtml(html);
string fkey = doc.DocumentNode.QuerySelector("form#login-form input[name=fkey]").Attributes["value"].Value;
string ssrc = doc.DocumentNode.QuerySelector("form#login-form input[name=ssrc]").Attributes["value"].Value;
Console.Write("Login: ");
string login = Console.ReadLine();
Console.Write("Password: ");
string password = ReadPassword();
Dictionary<string, string> formData = new Dictionary<string, string>
{
{ "fkey", fkey },
{ "ssrc", ssrc },
{ "email", login },
{ "password", password },
{ "oauth_version", "" },
{ "oauth_server", "" }
};
html = await HttpManager.PostFormAsync("https://ru.stackoverflow.com/users/login?ssrc=head&returnurl=https%3a%2f%2fru.stackoverflow.com%2f", formData);
doc = new HtmlDocument();
doc.LoadHtml(html);
}
string user = doc.DocumentNode.QuerySelector(".top-bar ol .my-profile span.v-visible-sr").InnerText;
Console.WriteLine(user);
string rep = HtmlEntity.DeEntitize(doc.DocumentNode.QuerySelector(".top-bar ol .my-profile div.-rep").Attributes["title"].Value);
Console.WriteLine(rep);
string badges = string.Join(Environment.NewLine, doc.DocumentNode.QuerySelectorAll(".top-bar ol .my-profile div.-badges span.v-visible-sr").Select(x => x.InnerText));
Console.WriteLine(badges);
await HttpManager.SaveCookiesAsync(filename);
Console.ReadKey();
}
private static string ReadPassword()
{
string password = string.Empty;
ConsoleKey key;
do
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
key = keyInfo.Key;
if (key == ConsoleKey.Backspace && password.Length > 0)
{
Console.Write("\b \b");
password = password[0..^1];
}
else if (!char.IsControl(keyInfo.KeyChar))
{
Console.Write("*");
password += keyInfo.KeyChar;
}
} while (key != ConsoleKey.Enter);
Console.WriteLine();
return password;
}
首次运行时的控制台输出要求提供凭据
Login: <my_email>@<censored>.ru
Password: ********************
aepot
ваша репутация: 4,904
2 золотых знака
5 серебряных знаков
25 бронзовых знаков
应用重启时输出到控制台,自动使用cookies进行授权
aepot
ваша репутация: 4,904
2 золотых знака
5 серебряных знаков
25 бронзовых знаков
无需检查可重现的示例,我知道它不考虑密码错误情况或帐户不支持密码登录。
顺便一提!为此,您需要在 StackExchange 帐户的安全设置中启用登录名和密码。
请看课程代码HttpManager
,有什么需要改进或做不同的吗?我以前没有使用过 Cookie。
更新- 使用BinaryFormatter
在@ヒミコ的建议下,我尝试了一个使用. 它也有效。好处 - 现在您不需要额外的方法来提取 cookie,但它会“按原样”保存。我还没有弄清楚哪个更好,但微软斥责它不安全。BinaryFormatter
CookieContainer
BinaryFormatter
BinaryFormatter
是不安全的,不能保证安全。有关详细信息,请参阅BinaryFormatter 安全指南。
public static class HttpManager
{
private static readonly HttpClientHandler handler = new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
AllowAutoRedirect = true
};
private static readonly HttpClient client = new HttpClient(handler)
{
DefaultRequestVersion = new Version(2, 0)
};
static HttpManager()
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0");
client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
}
public static async Task<string> GetPageAsync(string url)
{
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
return await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
}
public static async Task<string> PostFormAsync(string url, Dictionary<string, string> data)
{
using HttpContent content = new FormUrlEncodedContent(data);
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
return await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
}
public static async Task LoadCookiesAsync(string filename)
{
if (File.Exists(filename))
{
using FileStream fs = File.OpenRead(filename);
byte[] IV = new byte[16];
await fs.ReadAsync(IV);
byte[] protectedKey = new byte[178];
await fs.ReadAsync(protectedKey);
using AesManaged aes = new AesManaged
{
Key = ProtectedData.Unprotect(protectedKey, IV, DataProtectionScope.CurrentUser),
IV = IV
};
using CryptoStream cs = new CryptoStream(fs, aes.CreateDecryptor(), CryptoStreamMode.Read, true);
handler.CookieContainer = new BinaryFormatter().Deserialize(cs) as CookieContainer ?? new CookieContainer();
}
}
public static async Task SaveCookiesAsync(string filename)
{
using AesManaged aes = new AesManaged();
using FileStream fs = File.Create(filename);
await fs.WriteAsync(aes.IV);
await fs.WriteAsync(ProtectedData.Protect(aes.Key, aes.IV, DataProtectionScope.CurrentUser));
using CryptoStream cs = new CryptoStream(fs, aes.CreateEncryptor(), CryptoStreamMode.Write, true);
new BinaryFormatter().Serialize(cs, handler.CookieContainer);
}
}
我试图弄清楚如何同时将从服务器接收到的数据写入反序列化器以及将来用作重复请求缓存的文件。我正在寻找一种解决方案,以免从服务器读取整个响应,也不会手动逐个缓冲区复制它。
这是我可怜的尝试,评论了对我不起作用的东西。一切都适用于注释行,但如果未注释,反序列化器会崩溃,原因很明显。
private static async Task<T> HttpAPIRequest<T>(string url, string path)
{
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
using Stream responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
//using FileStream fileStream = File.Create(path);
//Task fileTask = responseStream.CopyToAsync(fileStream);
ValueTask<T> jsonTask = JsonSerializer.DeserializeAsync<T>(responseStream);
//await fileTask;
return await jsonTask;
}
但是怎么做才对呢?
我现在有一个工作代码,但是我首先将响应完全加载到byte[]
一个数组中,然后将该数组发送到反序列化器和文件。
前言
我正在为我解决一个复杂的数据可视化问题,项目很大并且需要重构,我被卡住了。在我为此功能做出架构决策之前,我无法重构。因此,我创建了一个单独的演示项目来重现我所需要的。
多选并不容易TreeView
,所以 github 上的现成解决方案不适合我,此外,我需要一个特殊的可视化,我已经能够在 XAML 标记中实现。唯一的问题是逻辑。
一个任务
TreeView
在可视化树中显示这些节点。可以选择(活动)或不选择(不活动)节点。为了可见性,这里是 INPC 接口的实现
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
数据
public enum TreeNodeType
{
Red,
Green,
Blue
}
public class TreeItem : NotifyPropertyChanged
{
private TreeNodeType _nodeType;
private int _nodeId;
private ObservableCollection<TreeItem> _items;
private bool _selected;
private bool _active;
public int NodeId
{
get => _nodeId;
set
{
_nodeId = value;
OnPropertyChanged();
}
}
public TreeNodeType NodeType
{
get => _nodeType;
set
{
_nodeType = value;
OnPropertyChanged();
}
}
public ObservableCollection<TreeItem> Items // TreeViewItem.ItemsSource
{
get => _items;
set
{
_items = value;
OnPropertyChanged();
}
}
public bool Selected // TreeViewItem.IsSelected
{
get => _selected;
set
{
_selected = value;
Active = value;
OnPropertyChanged();
}
}
public bool Active // моя попытка выделять элемент
{
get => _active;
set
{
if (value) _active = !_active;
OnPropertyChanged();
}
}
}
查看模型
public class MainViewModel : NotifyPropertyChanged
{
private ObservableCollection<TreeItem> _treeItems;
private ObservableCollection<int> _selectedItems;
public ObservableCollection<TreeItem> TreeItems
{
get => _treeItems;
set
{
_treeItems = value;
OnPropertyChanged();
}
}
public ObservableCollection<int> SelectedItems // сюда хочу записать айдищшники выбранных нод
{
get => _selectedItems;
set
{
_selectedItems = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
// тестовые данные
TreeItems = new ObservableCollection<TreeItem>
{
new TreeItem
{
NodeId = 0,
NodeType = TreeNodeType.Red,
Items = new ObservableCollection<TreeItem>
{
new TreeItem
{
NodeId = 1,
NodeType = TreeNodeType.Red,
Items = new ObservableCollection<TreeItem>
{
new TreeItem { NodeId = 2, NodeType = TreeNodeType.Green },
new TreeItem { NodeId = 3, NodeType = TreeNodeType.Red }
}
},
new TreeItem { NodeId = 4, NodeType = TreeNodeType.Red }
},
Selected = true
},
new TreeItem
{
NodeId = 5,
NodeType = TreeNodeType.Blue,
Items = new ObservableCollection<TreeItem>
{
new TreeItem
{
NodeId = 6,
NodeType = TreeNodeType.Blue,
Items = new ObservableCollection<TreeItem>
{
new TreeItem { NodeId = 7, NodeType = TreeNodeType.Blue }
}
},
new TreeItem { NodeId = 8, NodeType = TreeNodeType.Green }
},
Selected = true
},
new TreeItem
{
NodeId = 9,
NodeType = TreeNodeType.Green,
Selected = true
}
};
SelectedItems = new ObservableCollection<int>();
}
}
标记
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TreeView ItemsSource="{Binding TreeItems}">
<TreeView.Resources>
<Style TargetType="{x:Type TreeView}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeView}">
<ItemsPresenter/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="TreeViewItem">
<Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="IsSelected" Value="{Binding Selected}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<StackPanel Margin="2" >
<Border BorderThickness="1" HorizontalAlignment="Center" Margin="2">
<Border.Style>
<Style TargetType="{x:Type Border}">
<Style.Triggers>
<DataTrigger Binding="{Binding Active}" Value="True">
<Setter Property="BorderBrush" Value="Black"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Rectangle Width="30" Height="30" Margin="2">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Style.Triggers>
<DataTrigger Binding="{Binding NodeType}" Value="Red">
<Setter Property="Fill" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding NodeType}" Value="Green">
<Setter Property="Fill" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding NodeType}" Value="Blue">
<Setter Property="Fill" Value="Blue"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
<TextBlock Text="{Binding NodeId}" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="White"/>
</Grid>
</Border>
</StackPanel>
<ItemsPresenter/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
</TreeView>
<ItemsControl Grid.Column="1" ItemsSource="{Binding SelectedItems}"/>
</Grid>
告诉我在哪里挖?如何找到先前选择的相同类型的节点,取消选择它,然后才选择当前的?