wenmo8 发布的文章

本文github源码附上:
https://github.com/yangshuqi1201/RabbitMQ.Core

【前言】
RabbitMQ提供了五种消息模型,分别是简单模型、工作队列模型、发布/订阅模型、路由模型和主题模型。‌‌

  • 简单模型
    (Simple)‌
    :在这种模式下,一个生产者将消息发送到一个队列,只有一个消费者监听并处理该队列中的消息。这种模型适用于简单的场景,但存在消息可能未被正确处理就被删除的风险。

  • 工作队列模型
    (Work Queue)‌
    :此模型允许多个消费者共同处理队列中的任务,实现负载均衡。生产者将消息发送到队列,多个消费者竞争获取并处理这些消息。这种模型适用于需要高效处理大量任务的场景。

  • ‌发布/订阅模型
    (Publish/Subscribe)‌
    :在这种模式下,生产者将消息发送到一个交换机,交换机以广播
    (Fanout)
    的形式将消息发送给所有订阅了相应队列的消费者。这种模型适用于需要广播消息给所有感兴趣消费者的场景。

  • 路由模型
    (Routing)‌
    :使用direct交换机,生产者发送消息时需要指定路由键,交换机根据路由键将消息路由到相应的队列。这种模型适用于需要对消息进行精确控制的场景。

  • ‌主题模型
    (Topics)‌
    :使用topic交换机,支持使用通配符进行模式匹配,生产者发送的消息可以通过特定的路由键匹配到多个队列。这种模型适用于需要灵活匹配消息的场景。

  • 这些模型在应用场景、消息传递方式和交换机使用上有所不同,用户可以根据具体需求选择合适的模型来优化系统的性能和可靠性。


在之前我使用RabbitMQ实现了Direct类型的交换机实现了基础的生产消费。
RabbitMQ的部署请见:
https://www.cnblogs.com/sq1201/p/18635209
这篇文章我将基于.NET8.0来实现RabbitMQ的广播模式,以及死信队列的基础用法。

【一】首先创建项目,安装RabbitMQ的包(此处我没有选择最新版,因为最新版全面使用异步,关于IModel也改为了IChannel,最新版的语法有待研究),我的项目结构如下:

在这里延伸一下Asp.netcore的小知识点,以实现灵活而强大的配置管理系统。

1. Microsoft.Extensions.Configuration.Abstractions
功能:

  • 提供配置的基础接口和抽象,定义了与应用程序配置相关的核心机制。

  • 是依赖注入(DI)配置的一部分,为应用程序提供了对配置源的抽象访问。
    包含的核心功能和接口:

  • IConfiguration: 表示应用程序的配置,支持按层级结构访问配置值。
    var value = configuration["MySetting"];

  • IConfigurationSection: 表示配置中的一个具体部分,用于访问嵌套的层级配置。
    var section = configuration.GetSection("MySection");
    var subValue = section["SubSetting"];

  • IConfigurationProvider: 表示一个提供配置值的源(如文件、环境变量等)。

  • IConfigurationRoot: 是配置的根对象,支持动态监控配置变更。

适用场景:

  • 基础配置系统的搭建。
  • 当你需要自定义自己的配置提供程序时(例如:将数据库或远程 API 作为配置源)。
    示例:
using Microsoft.Extensions.Configuration;

var builder = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables();

var configuration = builder.Build();
Console.WriteLine(configuration["MySetting"]);

2. Microsoft.Extensions.Configuration.Binder

功能:

  • 提供了扩展方法,用于 将配置绑定到强类型对象。
  • 在读取配置时,通常需要将配置值转换为 C# 对象(例如类或结构),而这个包提供了关键的绑定功能。

包含的核心功能:

  • Bind 方法
    : 将配置值绑定到自定义类型。
var myOptions = new MyOptions();
configuration.Bind("MySection", myOptions);
  • Get 方法:
    从配置中直接返回类型化对象。
var myOptions = configuration.Get<MyOptions>("MySection");
  • GetValue 方法:
    直接从配置中获取某个特定的值,并将其转换为指定的类型。
int timeout = configuration.GetValue<int>("Timeout");

适用场景:

  • 强类型配置的支持:
    当需要将配置文件内容(如 JSON、环境变量)与代码中的类型对应时。
  • 简化复杂配置读取逻辑。
    示例:
using Microsoft.Extensions.Configuration;

public class MyOptions
{
    public string Setting1 { get; set; }
    public int Setting2 { get; set; }
}

// 读取配置
var builder = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json");

var configuration = builder.Build();

// 将配置绑定到强类型对象
var options = new MyOptions();
configuration.Bind("MySection", options);

Console.WriteLine(options.Setting1);
Console.WriteLine(options.Setting2);

// 或者直接获取类型化对象
var options2 = configuration.Get<MyOptions>("MySection");

【二】编写appsettings.json文件

【三】定义有关于配置文件信息的DTO

【四】编写RabbitMQ生产消费通用类,RabbitMQManager类。

点击查看代码
using RabbitMQ.Client.Events;
using RabbitMQ.Client;
using RabbitMQ.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;

namespace RabbitMQ.Service
{
    public class RabbitMQManager
    {
        //使用数组的部分,是给消费端用的,目前生产者只设置了一个,消费者可能存在多个。
        /// <summary>
        /// RabbitMQ工厂发送端
        /// </summary>
        private IConnectionFactory _connectionSendFactory;
        /// <summary>
        /// RabbitMQ工厂接收端
        /// </summary>
        private IConnectionFactory _connectionReceiveFactory;

        /// <summary>
        /// 连接 发送端
        /// </summary>
        private IConnection _connectionSend;
        /// <summary>
        /// 连接 消费端
        /// </summary>
        private IConnection[] _connectionReceive;

        /// <summary>
        /// MQ配置信息
        /// </summary>
        public MqConfigInfo _mqConfigs;

        /// <summary>
        /// 通道 发送端
        /// </summary>
        private IModel _modelSend;
        /// <summary>
        /// 通道 消费端
        /// </summary>
        private IModel[] _modelReceive;

        /// <summary>
        /// 事件
        /// </summary>
        private EventingBasicConsumer[] _basicConsumer;

        /// <summary>
        /// 消费者个数
        /// </summary>
        public int _costomerCount;
        public RabbitMQManager(IConfiguration configuration)
        {
            _mqConfigs = new MqConfigInfo
            {
                Host = configuration["MQ:Host"],
                Port = Convert.ToInt32(configuration["MQ:Port"]),
                User = configuration["MQ:User"],
                Password = configuration["MQ:Password"],
                ExchangeName = configuration["MQ:ExchangeName"],
                DeadLetterExchangeName = configuration["MQ:DeadLetterExchangeName"],
                DeadLetterQueueName = configuration["MQ:Queues:2:QueueName"]
            };
        }

        /// <summary>
        /// 初始化生产者连接
        /// </summary>
        public void InitProducerConnection()
        {
            Console.WriteLine("【开始】>>>>>>>>>>>>>>>生产者连接");

            _connectionSendFactory = new ConnectionFactory
            {
                HostName = _mqConfigs.Host,
                Port = _mqConfigs.Port,
                UserName = _mqConfigs.User,
                Password = _mqConfigs.Password
            };
            if (_connectionSend != null && _connectionSend.IsOpen)
            {
                return; //已有连接
            }

            _connectionSend = _connectionSendFactory.CreateConnection(); //创建生产者连接

            if (_modelSend != null && _modelSend.IsOpen)
            {
                return; //已有通道
            }

            _modelSend = _connectionSend.CreateModel(); //创建生产者通道

            // 声明主交换机 为 Fanout 类型,持久化
            _modelSend.ExchangeDeclare(
                exchange: _mqConfigs.ExchangeName,
                type: ExchangeType.Fanout,
                durable: true, // 明确设置为持久化
                autoDelete: false,
                arguments: null
            );

            // 声明死信交换机 为Fanout类型,持久化
            _modelSend.ExchangeDeclare(
                exchange: _mqConfigs.DeadLetterExchangeName,
                type: ExchangeType.Fanout,
                durable: true, // 明确设置为持久化
                autoDelete: false,
                arguments: null
            );

            // 声明死信队列
            _modelSend.QueueDeclare(
                queue: _mqConfigs.DeadLetterQueueName,
                durable: true,
                exclusive: false,
                autoDelete: false,
                arguments: null
            );

            // 绑定死信队列到死信交换机
            _modelSend.QueueBind(_mqConfigs.DeadLetterQueueName, _mqConfigs.DeadLetterExchangeName, routingKey: "");

            Console.WriteLine("【结束】>>>>>>>>>>>>>>>生产者连接");
        }


        /// <summary>
        /// 消息发布到交换机(Fanout模式)
        /// </summary>
        /// <param name="message">消息内容</param>
        /// <param name="exchangeName">交换机名称</param>
        /// <returns>发布结果</returns>
        public async Task<(bool Success, string ErrorMessage)> PublishAsync(string message, string exchangeName)
        {
            try
            {
                byte[] body = Encoding.UTF8.GetBytes(message);

                await Task.Run(() =>
                {
                    _modelSend.BasicPublish(
                        exchange: exchangeName,
                        routingKey: string.Empty, // Fanout 模式无需 RoutingKey
                        basicProperties: null,
                        body: body
                    );
                });

                return (true, string.Empty);
            }
            catch (Exception ex)
            {
                return (false, $"发布消息时发生错误: {ex.Message}");
            }
        }

        /// <summary>
        /// 消费者初始化连接配置
        /// </summary>
        public void InitConsumerConnections(List<QueueConfigInfo> queueConfigs)
        {
            Console.WriteLine("【开始】>>>>>>>>>>>>>>>消费者连接");

            //创建单个连接工厂
            _connectionReceiveFactory = new ConnectionFactory
            {
                HostName = _mqConfigs.Host,
                Port = _mqConfigs.Port,
                UserName = _mqConfigs.User,
                Password = _mqConfigs.Password
            };
            _costomerCount = queueConfigs.Sum(q => q.ConsumerCount); // 获取所有队列的消费者总数

            // 初始化数组         
            _connectionReceive = new IConnection[_costomerCount];
            _modelReceive = new IModel[_costomerCount];
            _basicConsumer = new EventingBasicConsumer[_costomerCount];

            int consumerIndex = 0; // 用于跟踪当前消费者索引

            foreach (var queueConfig in queueConfigs)
            {
                for (int i = 0; i < queueConfig.ConsumerCount; i++)
                {
                    string queueName = queueConfig.QueueName;

                    // 创建连接
                    _connectionReceive[consumerIndex] = _connectionReceiveFactory.CreateConnection();
                    _modelReceive[consumerIndex] = _connectionReceive[consumerIndex].CreateModel();
                    _basicConsumer[consumerIndex] = new EventingBasicConsumer(_modelReceive[consumerIndex]);

                    // 声明主交换机(确保交换机存在)
                    _modelReceive[consumerIndex].ExchangeDeclare(_mqConfigs.ExchangeName, ExchangeType.Fanout, durable: true, autoDelete: false, arguments: null);

                    // 声明死信交换机为 Fanout 类型
                    _modelReceive[consumerIndex].ExchangeDeclare(_mqConfigs.DeadLetterExchangeName, ExchangeType.Fanout, durable: true, autoDelete: false, arguments: null);

                    if (queueName == _mqConfigs.DeadLetterQueueName)
                    {
                        // 死信队列的声明和绑定
                        _modelReceive[consumerIndex].QueueDeclare(
                            queue: queueName,
                            durable: true,
                            exclusive: false,
                            autoDelete: false,
                            arguments: null
                        );

                        // 只将死信队列绑定到死信交换机
                        _modelReceive[consumerIndex].QueueBind(queueName, _mqConfigs.DeadLetterExchangeName, routingKey: "");
                    }
                    else
                    {
                        // 业务队列的声明和绑定
                        _modelReceive[consumerIndex].QueueDeclare(
                            queue: queueName,
                            durable: true,
                            exclusive: false,
                            autoDelete: false,
                            arguments: new Dictionary<string, object>
                            {
                        { "x-dead-letter-exchange", _mqConfigs.DeadLetterExchangeName },
                        { "x-dead-letter-routing-key", "" }
                            }
                        );

                        // 只将业务队列绑定到主交换机
                        _modelReceive[consumerIndex].QueueBind(queueName, _mqConfigs.ExchangeName, routingKey: "");
                    }

                    

                    // 设置QoS,确保每次只处理一个消息
                    _modelReceive[consumerIndex].BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);

                    consumerIndex++;
                }
            }

            Console.WriteLine("【结束】>>>>>>>>>>>>>>>消费者连接初始化完成");
        }

        /// <summary>
        /// 消费者连接
        /// </summary>
        public async Task ConncetionReceive(int consumeIndex, string exchangeName, string queueName, Func<string, Task> action)
        {

            await StartListenerAsync(async (model, ea) =>
            {
                try
                {
                    byte[] message = ea.Body.ToArray();
                    string msg = Encoding.UTF8.GetString(message);
                    Console.WriteLine($"队列 {queueName},消费者索引 {consumeIndex} 接收到消息:{msg}");

                    await action(msg);
                    _modelReceive[consumeIndex].BasicAck(ea.DeliveryTag, true);//确认消息
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"处理消息时发生错误: {ex.Message}");
                    // 拒绝消息且不重新入队,触发死信机制
                    _modelReceive[consumeIndex].BasicNack(ea.DeliveryTag, false, false);
                }

            }, queueName, consumeIndex);
        }

        /// <summary>
        /// 手动确认消费机制
        /// </summary>
        /// <param name="handler"></param>
        /// <param name="queueName"></param>
        /// <param name="consumeIndex"></param>
        /// <returns></returns>
        private async Task StartListenerAsync(AsyncEventHandler<BasicDeliverEventArgs> handler, string queueName, int consumeIndex)
        {
            _basicConsumer[consumeIndex].Received += async (sender, ea) => await handler(sender, ea);
            _modelReceive[consumeIndex].BasicConsume(
                queue: queueName,
                autoAck: false,
                consumer: _basicConsumer[consumeIndex]
            );

            Console.WriteLine($"队列 {queueName} 的消费者 {consumeIndex} 已启动监听");
        }


    }
}

【五】编写 RabbitMQService服务类,负责初始化 RabbitMQManager,并调用其方法完成 RabbitMQ 的连接和配置。

using Microsoft.Extensions.Configuration;
using RabbitMQ.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace RabbitMQ.Service
{
    public class RabbitMQService
    {
        private readonly RabbitMQManager _rabbitmqManager;

        public RabbitMQService(IConfiguration configuration)
        {
            _rabbitmqManager = new RabbitMQManager(configuration);

           
            //初始化生产者连接
            _rabbitmqManager.InitProducerConnection();

            var queueConfigs = configuration.GetSection("MQ:Queues").Get<List<QueueConfigInfo>>();

            //初始化消费者连接
            _rabbitmqManager.InitConsumerConnections(queueConfigs);
        }

        public RabbitMQManager Instance => _rabbitmqManager;
    }
}

【六】编写ActionService服务类,来实现模拟的消费者调用方法以及死信队列处理死信消息的逻辑

点击查看代码
namespace RabbitMQ.Service
{
    public class ActionService
    {
        /// <summary>
        /// 付款
        /// </summary>
        public async Task ExActionOne(string msg)
        {      
            Console.WriteLine($"消费成功了【{msg}】消息以后正在执行付款操作");
            // 模拟失败条件
            if (msg.Contains("fail"))
            {
                throw new Exception("Simulated processing failure in ExActionOne");
            }
            await Task.Delay(1000); // 替换 Thread.Sleep
        }
        /// <summary>
        /// 库存扣减
        /// </summary>
        public async Task ExActionTwo(string msg)
        {
            Console.WriteLine($"消费成功了【{msg}】消息以后正在执行库存扣减操作");
            // 模拟失败条件
            if (msg.Contains("fail"))
            {
                throw new Exception("Simulated processing failure in ExActionTwo");
            }
            await Task.Delay(1000); // 替换 Thread.Sleep
        }
        /// <summary>
        /// 处理死信队列的消息
        /// </summary>
        public async Task ExActionDeadLetter(string message)
        {
            Console.WriteLine($"处理死信消息: {message}");
            // 在这里可以记录日志、发送通知等
            await Task.Delay(1000);
        }
    }
}

【七】在启动项Program文件中注入我们的服务注册为单例模式

【八】在Webapi控制器中编写测试方法,模拟实现给主交换机发送消息,当消费失败的时候,消息被发送到死信队列由死信队列消费进行后续操作

namespace RabbitMQ.Core.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class TestController : ControllerBase
    {
        private readonly RabbitMQService _rabbitmqService;
        private readonly IConfiguration _configuration;

        public TestController(RabbitMQService rabbitmqService, IConfiguration configuration)
        {
            _rabbitmqService = rabbitmqService;
            _configuration = configuration;
        }
        /// <summary>
        /// 测试rabbitmq发送消息
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> TestRabbitMqPublishMessage(string pubMessage)
        {

            var result = await _rabbitmqService.Instance.PublishAsync(
                pubMessage,
                _configuration["MQ:ExchangeName"]
            );

            if (!result.Success)
            {
                Console.WriteLine($"【生产者】消息发送失败:{result.ErrorMessage}");
            }

            Console.WriteLine("【生产者】消息发送完成");

            return Ok();
        }
    }
}

【九】演示结果

PS:
我这里的死信交换机声明的还是广播模式,其实对于死信队列来说,交换机声明为Direct模式,使用RouteKey去匹配队列也是完全没问题的,而且针对于一条消息,不同队列有不同的消费结果,具体实现场景是所有队列的消费者都消费失败以后才算是一个失败的消息还是说有队列消费成功就不算是失败消息,这都是要结合实际业务场景去进行构思的。

前言

在.NET 9发布以后ASP.NET Core官方团队发布公告已经将
Swashbuckle.AspNetCore(一个为ASP.NET Core API提供Swagger工具的项目)
从ASP.NET Core Web API模板中移除,这意味着以后我们创建Web API项目的时候不会再自动生成Swagger API文档了。那么今天咱们一起来试试把我们的EasySQLite .NET 9的项目使用Scalar用于交互式API文档。

  • https://github.com/dotnet/aspnetcore/discussions/58103

Scalar介绍

Scalar是一个功能强大、易于使用的API客户端和文档生成工具,适用于各种规模的API项目,支持多种编程语言和平台。

  • scalar:https://github.com/scalar/scalar
  • scalar.aspnetcore:https://github.com/scalar/scalar/tree/main/packages/scalar.aspnetcore

下载EasySQLite项目

EasySQLite是一个.NET 9操作SQLite入门到实战的详细教程,主要是对学校班级,学生信息进行管理维护。

  • 下载地址:https://github.com/YSGStudyHards/EasySQLite
git clone https://github.com/YSGStudyHards/EasySQLite.git

安装 Scalar.AspNetCore 包

在NuGet包管理器中搜索:
Scalar.AspNetCore
(支持.NET 8和.NET 9)选择安装:

安装 Microsoft.AspNetCore.OpenApi 包

用于添加OpenApi服务,这是Scalar所需的:

在 Program 中配置

            // 添加OpenApi服务,这是Scalar所需的
            builder.Services.AddOpenApi(options =>
            {
                options.AddDocumentTransformer((document, context, cancellationToken) =>
                {
                    document.Info = new()
                    {
                        Title = "EasySQLite API",
                        Version = "V1",
                        Description = ".NET 8操作SQLite入门到实战"
                    };
                    return Task.CompletedTask;
                });
            });
            
            // 在开发环境中启用Scalar
            if (app.Environment.IsDevelopment())
            {
                app.MapScalarApiReference();//映射Scalar的API参考文档路径
                app.MapOpenApi();//映射OpenApi文档路径
            }

查看Scalar交互式API文档

在访问端口后面增加
scalar/v1
即可查看效果:

  • https://localhost:7240/scalar/v1

前言

如果我们需要观察程序运行过程中,某一个变量、某一个序列的变化情况,你可以在修改的地方打断点 debug,或者直接在需要的地方输出就行了。

但是对于一些树形结构,我们不好将其直观地呈现出来,常常只是输出每一个结点的值,但是这就丢失了结点之间的连边情况。有时候不得不手动画图。

所以我们经常累死。

于是,为了让我们活着,我想到了一种轻量级的,在终端直观呈现树形结构的方法。

正文

经典例子

回顾如下场景:

  • Windows 下命令行中,我们使用
    tree
    来观察目录结构。

比如,在某一目录下,使用
tree /A /F
的输出如下:

+---.vscode
|       launch.json
|
+---blog-prettier
|       LICENSE
|       README.md
|
+---web server
|   |   checkstatues.log
|   |   client.html
|   |   data.txt
|   |   gen-key.py
|   |   main_service.log
|   |   script-obfsed.js
|   |   test.html
|   |
|   \---fetch-new-url
|       |   README.md
|       |
|       \---docs
|               test
|
\---test
        a.html
        b.html
        index.html
        script.js
        style.css

这种经典的方法显然可以运用到我们的调试中。

分析

二叉树

我们不妨来考虑简单的二叉树,例如线段树、Treap、Splay 等平衡树。

我们考虑一种最简单的递归过程,仅在参数中传递输出的前缀。简单码出以下代码:

void output(int x, string pre) {
    cout << pre << "-" << x << ": " << tr[x].val << endl;
    if (!x) return;
    output(tr[x].son[1], pre + "   |");
    output(tr[x].son[0], pre + "   |");
}

void output() {
    output(root, ">");
}

这里先输出再
return
是为了让输出的二叉树更好看,不然遇到一个孩子不知道是左儿子还是右儿子。

将右儿子作为第一个儿子输出,是为了符合二叉查找树。

可能的输出:一棵不断插入的 Splay
>-1: 1
>   |-0: 0
>   |-0: 0
>-2: 1
>   |-1: 1
>   |   |-0: 0
>   |   |-0: 0
>   |-0: 0
>-3: 4
>   |-0: 0
>   |-1: 1
>   |   |-0: 0
>   |   |-2: 1
>   |   |   |-0: 0
>   |   |   |-0: 0
>-4: 5
>   |-0: 0
>   |-3: 4
>   |   |-0: 0
>   |   |-1: 1
>   |   |   |-0: 0
>   |   |   |-2: 1
>   |   |   |   |-0: 0
>   |   |   |   |-0: 0
>-5: 1
>   |-3: 4
>   |   |-4: 5
>   |   |   |-0: 0
>   |   |   |-0: 0
>   |   |-2: 1
>   |   |   |-1: 1
>   |   |   |   |-0: 0
>   |   |   |   |-0: 0
>   |   |   |-0: 0
>   |-0: 0
>-6: 4
>   |-3: 4
>   |   |-4: 5
>   |   |   |-0: 0
>   |   |   |-0: 0
>   |   |-0: 0
>   |-5: 1
>   |   |-1: 1
>   |   |   |-0: 0
>   |   |   |-2: 1
>   |   |   |   |-0: 0
>   |   |   |   |-0: 0
>   |   |-0: 0

这对于考场上调试来说已经足够了,仅需将头逆时针旋转
\(45^\circ\)
就能看到一棵完美的二叉树了。你可以在每个结点之后输出更多的信息。

但是,我们怎样达到更完美的效果呢,比如第二个孩子之前不输出树杈、第二个孩子后输出空行(多个第二个孩子仅输出一个空行)等等。

我们仅需多记录是否是第一个孩子即可。

void output(int x, string pre, bool firstSon) {
    cout << pre << (firstSon ? "+" : "\\") << "---" << x << ": " << tr[x].val << endl;
    if (!x) return;
    pre += firstSon ? "|" : " ";
    output(tr[x].son[1], pre + "   ", true);
    output(tr[x].son[0], pre + "   ", false);
    if (firstSon) cout << pre << endl;
}

void output() {
    output(root, "", false);
}

效果见文末。

多叉树

多叉树就只能是 LCT 了吧,还有什么扭曲的树你必须要打印出来的?

虽然好像打印出来还是不方便调试……

我们加以改进,由于有了虚实链之分,我们在空节点不直接
return
,而是输出一条边。然后把是否是第一个孩子,变成是否是最后一个孩子。

代码:

vector<int> edge[N];

void output(int x, string pre, bool lastSon, bool real) {
    cout << pre << (!lastSon ? "+" : "\\") << "---";
    if (x) cout << x << ": " << tr[x].val << endl;
    else cout << "null" << endl;
    pre += !lastSon ? (real ? "|" : "`") : " ";
    if (x && (tr[x].son[0] || tr[x].son[1] || edge[x].size())) {
        pushdown(x);
        output(tr[x].son[1], pre + "   ", false, true);
        output(tr[x].son[0], pre + "   ", edge[x].empty(), false);
        for (int y : edge[x])
            output(y, pre + "   ", y == edge[x].back(), false);
    }
    if (!lastSon) cout << pre << endl;
}

void output(int n) {
    for (int i = 1; i <= n; ++i)
        edge[i].clear();
    for (int i = 1; i <= n; ++i)
        if (isRoot(i))
            edge[tr[i].fa].emplace_back(i);
    cout << "==== LCT forest ====" << endl;
    for (int i = 1; i <= n; ++i)
        if (!tr[i].fa)
            output(i, "", true, false);
    cout << "====================" << endl;
}

效果见文末。

代码

二叉树
void output(int x, string pre, bool firstSon) {
    cout << pre << (firstSon ? "+" : "\\") << "---" << x << ": " << tr[x].val << endl;
    if (!x) return;
    pre += firstSon ? "|" : " ";
    output(tr[x].son[1], pre + "   ", true);
    output(tr[x].son[0], pre + "   ", false);
    if (firstSon) cout << pre << endl;
}

void output() {
    output(root, "", false);
}
多叉树 LCT
vector<int> edge[N];

void output(int x, string pre, bool lastSon, bool real) {
    cout << pre << (!lastSon ? "+" : "\\") << "---";
    if (x) cout << x << ": " << tr[x].val << endl;
    else cout << "null" << endl;
    pre += !lastSon ? (real ? "|" : "`") : " ";
    if (x && (tr[x].son[0] || tr[x].son[1] || edge[x].size())) {
        pushdown(x);
        output(tr[x].son[1], pre + "   ", false, true);
        output(tr[x].son[0], pre + "   ", edge[x].empty(), false);
        for (int y : edge[x])
            output(y, pre + "   ", y == edge[x].back(), false);
    }
    if (!lastSon) cout << pre << endl;
}

void output(int n) {
    for (int i = 1; i <= n; ++i)
        edge[i].clear();
    for (int i = 1; i <= n; ++i)
        if (isRoot(i))
            edge[tr[i].fa].emplace_back(i);
    cout << "==== LCT forest ====" << endl;
    for (int i = 1; i <= n; ++i)
        if (!tr[i].fa)
            output(i, "", true, false);
    cout << "====================" << endl;
}

输出效果

可能的输出:一棵不断插入的 Splay
\---1: 1
    +---0: 0
    \---0: 0
\---2: 1
    +---1: 1
    |   +---0: 0
    |   \---0: 0
    |
    \---0: 0
\---3: 4
    +---0: 0
    \---1: 1
        +---0: 0
        \---2: 1
            +---0: 0
            \---0: 0
\---4: 5
    +---0: 0
    \---3: 4
        +---0: 0
        \---1: 1
            +---0: 0
            \---2: 1
                +---0: 0
                \---0: 0
\---5: 1
    +---3: 4
    |   +---4: 5
    |   |   +---0: 0
    |   |   \---0: 0
    |   |
    |   \---2: 1
    |       +---1: 1
    |       |   +---0: 0
    |       |   \---0: 0
    |       |
    |       \---0: 0
    |
    \---0: 0
\---6: 4
    +---3: 4
    |   +---4: 5
    |   |   +---0: 0
    |   |   \---0: 0
    |   |
    |   \---0: 0
    |
    \---5: 1
        +---1: 1
        |   +---0: 0
        |   \---2: 1
        |       +---0: 0
        |       \---0: 0
        |
        \---0: 0
可能的输出:一棵带有左右边界的不断插入的 Treap
\---2: inf
    +---0: 0
    \---1: -inf
        +---3: 1
        |   +---0: 0
        |   \---0: 0
        |
        \---0: 0
\---2: inf
    +---0: 0
    \---1: -inf
        +---3: 1
        |   +---0: 0
        |   \---0: 0
        |
        \---0: 0
\---2: inf
    +---0: 0
    \---1: -inf
        +---3: 1
        |   +---4: 4
        |   |   +---0: 0
        |   |   \---0: 0
        |   |
        |   \---0: 0
        |
        \---0: 0
\---2: inf
    +---0: 0
    \---1: -inf
        +---3: 1
        |   +---5: 5
        |   |   +---0: 0
        |   |   \---4: 4
        |   |       +---0: 0
        |   |       \---0: 0
        |   |
        |   \---0: 0
        |
        \---0: 0
\---2: inf
    +---0: 0
    \---1: -inf
        +---3: 1
        |   +---5: 5
        |   |   +---0: 0
        |   |   \---4: 4
        |   |       +---0: 0
        |   |       \---0: 0
        |   |
        |   \---0: 0
        |
        \---0: 0
\---2: inf
    +---0: 0
    \---1: -inf
        +---3: 1
        |   +---5: 5
        |   |   +---0: 0
        |   |   \---4: 4
        |   |       +---0: 0
        |   |       \---0: 0
        |   |
        |   \---0: 0
        |
        \---0: 0
可能的输出:一棵不断插入的无旋 Treap
\---1: 1
    +---0: 0
    \---0: 0
\---1: 1
    +---0: 0
    \---2: 1
        +---0: 0
        \---0: 0
\---3: 4
    +---0: 0
    \---1: 1
        +---0: 0
        \---2: 1
            +---0: 0
            \---0: 0
\---3: 4
    +---4: 5
    |   +---0: 0
    |   \---0: 0
    |
    \---1: 1
        +---0: 0
        \---2: 1
            +---0: 0
            \---0: 0
\---5: 1
    +---3: 4
    |   +---4: 5
    |   |   +---0: 0
    |   |   \---0: 0
    |   |
    |   \---1: 1
    |       +---0: 0
    |       \---2: 1
    |           +---0: 0
    |           \---0: 0
    |
    \---0: 0
\---5: 1
    +---6: 4
    |   +---3: 4
    |   |   +---4: 5
    |   |   |   +---0: 0
    |   |   |   \---0: 0
    |   |   |
    |   |   \---0: 0
    |   |
    |   \---1: 1
    |       +---0: 0
    |       \---2: 1
    |           +---0: 0
    |           \---0: 0
    |
    \---0: 0
可能的输出:一棵动态开点线段树
\---[1, 5]: 1
    +---[1, 3]: 0
    \---[4, 5]: 1
        +---[4, 4]: 0
        \---[5, 5]: 1
\---[1, 5]: 6
    +---[1, 3]: 0
    \---[4, 5]: 6
        +---[4, 4]: 0
        \---[5, 5]: 6
\---[1, 5]: 10
    +---[1, 3]: 0
    \---[4, 5]: 10
        +---[4, 4]: 4
        \---[5, 5]: 6
\---[1, 5]: 12
    +---[1, 3]: 2
    |   +---[1, 2]: 0
    |   \---[3, 3]: 2
    |
    \---[4, 5]: 10
        +---[4, 4]: 4
        \---[5, 5]: 6
\---[1, 5]: 15
    +---[1, 3]: 5
    |   +---[1, 2]: 3 (with lazy = 3)
    |   |   +---[1, 1]: 0
    |   |   \---[2, 2]: 0
    |   |
    |   \---[3, 3]: 2
    |
    \---[4, 5]: 10
        +---[4, 4]: 4
        \---[5, 5]: 6
\---[1, 5]: 15
    +---[1, 3]: 5
    |   +---[1, 2]: 3 (with lazy = 3)
    |   |   +---[1, 1]: 0
    |   |   \---[2, 2]: 0
    |   |
    |   \---[3, 3]: 2
    |
    \---[4, 5]: 10
        +---[4, 4]: 4
        \---[5, 5]: 6
\---[1, 5]: 19
    +---[1, 3]: 5
    |   +---[1, 2]: 3 (with lazy = 3)
    |   |   +---[1, 1]: 0
    |   |   \---[2, 2]: 0
    |   |
    |   \---[3, 3]: 2
    |
    \---[4, 5]: 14
        +---[4, 4]: 6
        \---[5, 5]: 8
\---[1, 5]: 19
    +---[1, 3]: 5
    |   +---[1, 2]: 3 (with lazy = 3)
    |   |   +---[1, 1]: 0
    |   |   \---[2, 2]: 0
    |   |
    |   \---[3, 3]: 2
    |
    \---[4, 5]: 14
        +---[4, 4]: 6
        \---[5, 5]: 8
\---[1, 5]: 24 (with lazy = 1)
    +---[1, 3]: 5
    |   +---[1, 2]: 3 (with lazy = 3)
    |   |   +---[1, 1]: 0
    |   |   \---[2, 2]: 0
    |   |
    |   \---[3, 3]: 2
    |
    \---[4, 5]: 14
        +---[4, 4]: 6
        \---[5, 5]: 8
\---[1, 5]: 24 (with lazy = 1)
    +---[1, 3]: 5
    |   +---[1, 2]: 3 (with lazy = 3)
    |   |   +---[1, 1]: 0
    |   |   \---[2, 2]: 0
    |   |
    |   \---[3, 3]: 2
    |
    \---[4, 5]: 14
        +---[4, 4]: 6
        \---[5, 5]: 8
可能的输出:一棵树状数组

这玩意你还要调试?

可能的输出:左偏树森林
==== 左偏树 1 ====
\---5: <4, 2>
    +---3: <4, 3>
    |   +---4: <5, 2>
    |   |   +---0: <0, 0>
    |   |   \---7: <9, 4>
    |   |       +---0: <0, 0>
    |   |       \---0: <0, 0>
    |   |
    |   \---0: <0, 0>
    |
    \---0: <0, 0>

==== 左偏树 2 ====
\---1: <3, 3>
    +---6: <8, 4>
    |   +---0: <0, 0>
    |   \---2: <9, 3>
    |       +---0: <0, 0>
    |       \---0: <0, 0>
    |
    \---0: <0, 0>

==== 左偏树 1 ====
\---5: <4, 2>
    +---3: <4, 3>
    |   +---4: <5, 2>
    |   |   +---0: <0, 0>
    |   |   \---7: <9, 4>
    |   |       +---0: <0, 0>
    |   |       \---0: <0, 0>
    |   |
    |   \---0: <0, 0>
    |
    \---1: <5, 3>
        +---6: <8, 4>
        |   +---0: <0, 0>
        |   \---2: <9, 3>
        |       +---0: <0, 0>
        |       \---0: <0, 0>
        |
        \---0: <0, 0>

==== 左偏树 1 ====
\---3: <4, 3>
    +---4: <5, 2>
    |   +---0: <0, 0>
    |   \---7: <9, 4>
    |       +---0: <0, 0>
    |       \---0: <0, 0>
    |
    \---1: <5, 3>
        +---6: <8, 4>
        |   +---0: <0, 0>
        |   \---2: <9, 3>
        |       +---0: <0, 0>
        |       \---0: <0, 0>
        |
        \---0: <0, 0>

==== 左偏树 1 ====
\---4: <5, 2>
    +---1: <5, 3>
    |   +---6: <10, 4>
    |   |   +---0: <0, 0>
    |   |   \---2: <9, 3>
    |   |       +---0: <0, 0>
    |   |       \---0: <0, 0>
    |   |
    |   \---7: <11, 4>
    |       +---0: <0, 0>
    |       \---0: <0, 0>
    |
    \---0: <0, 0>

==== 左偏树 1 ====
\---1: <5, 3>
    +---6: <10, 4>
    |   +---0: <0, 0>
    |   \---2: <9, 3>
    |       +---0: <0, 0>
    |       \---0: <0, 0>
    |
    \---7: <11, 4>
        +---0: <0, 0>
        \---0: <0, 0>

==== 左偏树 1 ====
\---6: <10, 4>
    +---2: <11, 3>
    |   +---0: <0, 0>
    |   \---7: <11, 4>
    |       +---0: <0, 0>
    |       \---0: <0, 0>
    |
    \---0: <0, 0>

==== 左偏树 1 ====
\---2: <11, 3>
    +---0: <0, 0>
    \---7: <11, 4>
        +---0: <0, 0>
        \---0: <0, 0>

==== 左偏树 1 ====
\---7: <11, 4>
    +---0: <0, 0>
    \---0: <0, 0>

==== 左偏树 1 ====
\---0: <0, 0>
可能的输出:Link Cut Tree
==== LCT forest ====
\---1: 114
\---2: 514
\---3: 19
\---4: 19
\---5: 810
====================

link 1 and 2 success

==== LCT forest ====
\---2: 514
    +---null
    |
    +---null
    `
    \---1: 114
\---3: 19
\---4: 19
\---5: 810
====================

cut 1 and 2 success

==== LCT forest ====
\---1: 114
\---2: 514
\---3: 19
\---4: 19
\---5: 810
====================

link 1 and 2 success

==== LCT forest ====
\---2: 514
    +---null
    |
    +---null
    `
    \---1: 114
\---3: 19
\---4: 19
\---5: 810
====================

link 2 and 3 success

==== LCT forest ====
\---3: 19
    +---null
    |
    +---null
    `
    \---2: 514
        +---null
        |
        +---null
        `
        \---1: 114
\---4: 19
\---5: 810
====================

cut 1 and 3 failed

==== LCT forest ====
\---1: 114
    +---2: 514
    |   +---3: 19
    |   |
    |   \---null
    |
    \---null
\---4: 19
\---5: 810
====================

link 1 and 3 failed

==== LCT forest ====
\---1: 114
    +---3: 19
    |   +---null
    |   |
    |   \---2: 514
    |
    \---null
\---4: 19
\---5: 810
====================

link 4 and 5 success

==== LCT forest ====
\---1: 114
    +---3: 19
    |   +---null
    |   |
    |   \---2: 514
    |
    \---null
\---5: 810
    +---null
    |
    +---null
    `
    \---4: 19
====================

link 2 and 5 success

==== LCT forest ====
\---5: 810
    +---null
    |
    +---null
    `
    +---2: 514
    `   +---1: 114
    `   |
    `   +---null
    `   `
    `   \---3: 19
    `
    \---4: 19
====================

modify value 5 to 233333 success

==== LCT forest ====
\---5: 233333
    +---null
    |
    +---null
    `
    +---2: 514
    `   +---1: 114
    `   |
    `   +---null
    `   `
    `   \---3: 19
    `
    \---4: 19
====================

access 3 success

==== LCT forest ====
\---5: 233333
    +---2: 514
    |   +---3: 19
    |   |
    |   +---null
    |   `
    |   \---1: 114
    |
    +---null
    `
    \---4: 19
====================

split 2 ~ 4 success

==== LCT forest ====
\---4: 19
    +---null
    |
    \---5: 233333
        +---null
        |
        \---2: 514
            +---null
            |
            +---null
            `
            +---1: 114
            `
            \---3: 19
====================

split 2 ~ 5 success

==== LCT forest ====
\---5: 233333
    +---null
    |
    +---2: 514
    `   +---null
    `   |
    `   +---null
    `   `
    `   +---1: 114
    `   `
    `   \---3: 19
    `
    \---4: 19
====================

一、前言

大家好!我是付工。

之前有个学员问了这样的一个问题:

学会了上位机,是不是就可以不用PLC了呢?

今天跟大家分享一下上位机能不能代替PLC?

二、网络架构

首先我们看下这张网络架构图。

从图中可以看到:

上位机属于过程监控层,PLC属于现场控制层。

PLC作为下位机,上位机与下位机进行通信,实现整个控制系统的运作。

因此正常情况下,上位机不是用来替代PLC的。

但是,有些情况下,尤其是在一些运动控制机器视觉的项目场景中,整个项目中并没有使用PLC,那么这个时候,我们能不能通过上位机来实现一些逻辑控制呢

三、实现原理

PLC全称可编程逻辑控制器,主要由输入输出(IO)+ 逻辑控制(程序)+ 外部接口(通信)三部分组成。

上位机本身可以实现一定的逻辑控制和外部通信功能,输入输出我们需采用IO采集卡,大部分运动控制卡也自带IO,然后在上位机编写相应的代码来实现逻辑控制即可。

PLC的实现原理,是通过不断地从上而下,从左而右来扫描PLC程序,同时对接一些IO输入输出。

通过上位机来实现的话,实现原理与之类似,我们可以通过单独的一个线程来实现扫描,而PLC里的每个指令,我们上位机都可以通过构造对应的类或方法来实现。

原理其实是相通的,之所以PLC实现逻辑控制比较容易,是因为厂家已经帮我们封装好了很多开箱即用的指令,我们直接调用即可。

所以,如果我们使用C#上位机做这种逻辑控制比较多,也可以封装一些沿信号检测、延时定时器等一些指令,比如起保停主要就是上升沿检测。

四、IO采集卡

目前市面上有很多IO采集卡,大部分IO采集卡都是通过串口ModbusRTU或者以太网ModbusTCP来实现通信,这种通信效率相对较低。

我这里采用的是正运动的IO采集卡,正运动的采集卡也支持ModbusTCP通信,而且通信效率较高,后续可以测试对比一下。

另外一点就是,如果我们使用过正运动的库,我们也可以通过官方提供的运动控制卡的库来直接对接,这样就不用自己写通信库或者使用第三方通信库了。

IO采集卡型号是ECI0032,ECI0032 是正运动技术开发的一款网络 IO 控制卡,采用优化的网络通讯协议可以实现实时的 IO 控制。


ECI0032 网络 IO 控制卡支持以太网口,RS232 通讯接口和电脑相连,通过 CAN 总线可以连接各个扩展模块,从而扩展输入输出点数。

相关参数如下所示:


ECI0032 板上自带 16 个通用输入口,16 个通用输出口(带过流保护)。

ECI0032 带 1 个 RS232 串口,1 个以太网接口。

ECI0032 带一个 CAN 总线接口,通过IO点数不够,可以通过 ZCAN 协议来连接扩展模块和控制器。

五、实现过程

基本的接线和配置,我这里就不详细阐述了,这里我将板卡的IP地址改为192.168.2.33。

通过一个按钮盒接了两个按钮和一个指示灯,分别表示启动、停止和状态显示。

采用Winform设计一个简单的界面,界面效果如下所示:

当连接上IO卡之后,开启一个多线程循环扫描,读取输入按钮的状态,当检测到启动按钮从False变为True的时候,给输出信号置位,当检测停止按钮从False变成True的时候,给输出信号复位。

核心代码如下所示:

这个写法有点类似于SCL的逻辑,整体实现效果如下:

动图封面

最后总结一下:虽然某些场合,我们可以不使用PLC,通过上位机结合IO采集卡实现一定的逻辑控制,但是并不代表上位机就可以替代PLC。PLC作为目前工业领域使用最广泛的控制器,很多功能是上位机无法替代的。

.NET Core:架构、特性和优势详解

在软件开发领域,保持领先地位至关重要。随着技术以指数级的速度发展,开发人员不断寻求高效、可扩展且多功能的解决方案来应对现代挑战。.NET Core 就是这样一种受到广泛关注的解决方案。在本指南中,我们将深入研究 .NET Core 的基础知识,探索其架构、功能以及相对于传统 .NET Framework 的优势。

了解 .NET Core

.NET Core 的核心是一个开源的跨平台框架,用于构建现代的、基于云的和与互联网连接的应用程序。它由 Microsoft 开发,是对 .NET Framework 的重新构想和重大演进。虽然 .NET Framework 主要针对 Windows,但 .NET Core 的设计与平台无关,支持 Windows、macOS 和 Linux 环境。这种跨平台功能开辟了一个充满可能性的世界,使开发人员能够创建可以在各种操作系统上无缝运行的应用程序。

.NET Core 的体系结构

  • .NET Core 的架构是模块化和轻量级的
    ,由一组针对性能和可扩展性进行了优化的库和运行时组件组成。.NET Core 的关键组件之一是 CoreCLR,它是执行用 .NET Core 编写的应用程序的运行时。它包括即时 (JIT) 编译器、垃圾收集器和其他用于管理内存和高效执行代码的基本组件。
  • .NET Core 架构的另一个重要方面是它支持多种应用程序模型
    ,包括控制台应用程序、Web 应用程序(通过 ASP.NET Core)和云原生微服务。这种灵活性使开发人员能够根据其特定需求选择正确的应用程序模型,无论是构建简单的命令行工具还是复杂的分布式应用程序架构。
1. 分层架构

.NET Core 的架构可以分为以下几个主要层次:

  • 应用层
    :这是开发者直接交互的层,包括各种应用程序和库。开发者可以使用 C#、F#、VB.NET 等语言编写代码。
  • 框架层
    :这一层提供了核心的运行时和库,包括 .NET Standard 库、ASP.NET Core、Entity Framework Core 等。这些库提供了丰富的功能,如数据访问、Web 开发、日志记录等。
  • 运行时层
    :.NET Core 运行时(CoreCLR)是 .NET Core 的核心组件,负责执行托管代码。它包括垃圾回收器(GC)、即时编译器(JIT)、线程管理等。
  • 操作系统抽象层
    :这一层提供了对不同操作系统的抽象,使得 .NET Core 可以在 Windows、Linux 和 macOS 上运行。
2. 模块化设计

.NET Core 采用了模块化设计,这意味着开发者可以根据需要选择性地使用不同的库和组件。这种设计使得 .NET Core 更加轻量级,并且可以根据不同的应用场景进行定制。

3. 跨平台支持

.NET Core 的一个重要特性是其跨平台支持。它可以在 Windows、Linux 和 macOS 上运行,并且提供了统一的 API 和工具链,使得开发者可以在不同的平台上使用相同的代码库。

特性

1. 高性能

.NET Core 通过多种技术手段提升了性能,包括:

  • 即时编译(JIT)
    :.NET Core 使用 RyuJIT 编译器,能够在运行时将 IL(中间语言)代码编译为本地代码,从而提高执行效率。
  • 垃圾回收(GC)
    :.NET Core 的垃圾回收器经过优化,能够更高效地管理内存,减少内存碎片和停顿时间。
  • 异步编程模型
    :.NET Core 提供了强大的异步编程支持,通过
    async

    await
    关键字,开发者可以编写高效的异步代码,充分利用系统资源。
public async Task<string> DownloadDataAsync(string url)
{
    using (HttpClient client = new HttpClient())
    {
        return await client.GetStringAsync(url);
    }
}
2. 开源和社区支持

.NET Core 是一个完全开源的项目,托管在 GitHub 上。开发者可以查看源代码、提交问题、贡献代码,甚至参与核心组件的开发。这种开放性使得 .NET Core 能够快速迭代和改进。

3. 丰富的生态系统

.NET Core 拥有一个庞大的生态系统,包括:

  • NuGet 包管理器
    :NuGet 是 .NET 的包管理器,提供了数以万计的库和工具,开发者可以轻松地集成到自己的项目中。
  • 开发工具
    :Visual Studio、Visual Studio Code、Rider 等开发工具都提供了对 .NET Core 的全面支持,包括代码编辑、调试、测试等功能。
  • 云服务集成
    :.NET Core 与 Azure、AWS 等云服务紧密集成,开发者可以轻松地将应用程序部署到云端,并利用云服务的各种功能。
4. 微服务和容器化支持

.NET Core 非常适合构建微服务架构的应用程序。它提供了轻量级的运行时和库,使得每个微服务可以独立开发、测试和部署。此外,.NET Core 对 Docker 容器有很好的支持,开发者可以将应用程序打包为 Docker 镜像,并在 Kubernetes 等容器编排平台上运行。

# Dockerfile 示例
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "./"]
RUN dotnet restore "./MyApp.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "MyApp.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]

.NET Core 的优势

.NET Core 具有大量功能,可简化开发流程并提高生产力。一些值得注意的特点包括:

1. 跨平台兼容性

.NET Core 的跨平台特性使得开发者可以在不同的操作系统上使用相同的代码库,减少了开发和维护的成本。无论是 Windows 桌面应用、Linux 服务器应用,还是 macOS 上的移动应用,.NET Core 都能提供一致的开发体验。

2. 高性能和可扩展性

.NET Core 的高性能特性使得它非常适合构建高并发、低延迟的应用程序。无论是 Web 应用、API 服务,还是后台任务,.NET Core 都能提供卓越的性能表现。此外,.NET Core 的可扩展性也非常出色,开发者可以通过添加自定义库和组件来扩展应用的功能。

3. 开源和社区支持

.NET Core 的开源特性使得它能够快速迭代和改进,社区的积极参与也使得 .NET Core 的生态系统更加丰富和多样化。开发者可以从社区中获得大量的资源和支持,包括文档、教程、示例代码等。

4. 云原生支持

.NET Core 对云原生应用的支持非常出色。它与 Azure、AWS 等云服务紧密集成,开发者可以轻松地将应用程序部署到云端,并利用云服务的各种功能,如自动扩展、负载均衡、监控等。此外,.NET Core 对 Docker 和 Kubernetes 的支持也使得它非常适合构建和运行容器化的应用程序。

5. 现代语言特性

.NET Core 支持现代语言特性和编程范式,包括异步编程、模式匹配和函数式编程结构,使开发人员能够编写更干净、更具表现力的代码。

public static int Fibonacci(int n) => n switch
{
    0 => 0,
    1 => 1,
    _ => Fibonacci(n - 1) + Fibonacci(n - 2)
};
6. 模块化设计

.NET Core 的模块化设计允许开发人员在其应用程序中仅包含必要的组件,从而减少整体占用空间并提高可维护性。

结论

.NET Core 代表着软件开发领域的一次重大飞跃,它提供了一个具有无与伦比的性能、可扩展性和多功能性的现代跨平台框架,适用于构建现代的、云端的、互联网连接的应用程序。它的跨平台支持、模块化设计、高性能特性以及丰富的生态系统,使得它成为开发者构建各种类型应用的理想选择。无论是初学者还是经验丰富的开发者,.NET Core 都能提供一个高效、灵活和可扩展的开发平台。