Jetpack Compose UI架构

news2024/12/23 9:40:33

Jetpack Compose UI架构

引言

Jetpack Compose是我职业生涯中最激动人心的事。它改变了我工作和问题思考的方式,引入了易用且灵活的工具,几乎可轻松实现各种功能。

早期在生产项目中尝试了Jetpack Compose后,我迅速着迷。尽管我已有使用Compose创建UI的经验,但对新的Jetpack Compose驱动特性的组织和架构引发了许多反复。

本文目标是分享这些经验,提出可扩展、易用、易操作的架构,并接受反馈以进一步改进。

免责声明:本文仅涉及UI部分,其余应用构建遵循经典Clean Architecture方法。假设您熟悉Jetpack Compose,不深入讨论UI实现细节。

示例

为了提供具体示例,让我介绍一下本文将要介绍的示范项目。我们将要构建的应用程序允许用户在不同的地标之间进行切换并导航到它们。以下是基本流程的描述:

  • 用户可以滑动浏览地点卡片,查看有关地点的不同信息,如地点图片、名称和评分。
  • 用户可以将地点标记/取消标记为收藏。
  • 用户可以从其位置导航并规划前往所选地点的路线。为此,我们需要用户的位置权限。
  • 如果出现错误,我们希望显示一个消息提示。
  • 权限只有在用户选择规划路线时才会询问。如果用户拒绝权限,我们会导航到另一个屏幕(位置理由屏幕)。
  • 我们还希望跟踪用户与分析服务的交互。

基础知识

我对 Jetpack Compose 的最初记忆是这个方程式:UI = f(state)。这意味着 UI 是应用于某个状态的函数的结果。让我们简要回顾一下 Compose 和响应式 UI 的重要方面,特别是关于状态处理的内容:状态提升和单向数据流。

状态提升
状态提升是一种在软件开发中常用的技术,尤其在 UI 编程中,它将组件管理和操作状态的责任移至更高级的组件或更集中的位置。状态提升的目的是改善代码组织、可重用性和可维护性。你可以在这里了解更多关于状态提升的内容。

单向数据流
单向数据流(UDF)是一种设计模式,其中状态向下流动,事件向上流动。遵循单向数据流,你可以将在 UI 中显示状态的可组合项与存储和更改状态的应用程序部分解耦。

要点是,我们希望我们的 UI 组件消耗状态并发出事件。如果让我们的组件处理源自外部的事件,将打破这一规则,引入多个真相来源。重要的是,我们引入的任何“事件”都应该基于状态。

入门

首先,让我们介绍核心组件,这些是我们架构的基础。

State

我们从最明显的开始,即状态。状态可以是根据你的用例而定的任何内容。它可以是一个数据类,包含UI可能需要的所有属性,或者是一个封装接口,代表所有可能的情景。无论哪种情况,状态是你的组件或整个屏幕UI的“静态”表示,便于轻松操作。
根据我们的要求,我们有一个地点列表和一个可选的错误,所以我们的状态可能是这样的:

data class PlacesState(
    val places: List<Place> = emptyList(),
    val error: String? = null
)

Screen

屏幕是我们方程中的 f 函数。为了遵循状态提升模式,我们需要使该组件无状态并将用户交互暴露为回调。这将使我们的屏幕具有可测试性、预览性和可重用性!
我们已经有了状态,基于我们的需求,我们只需要处理两个用户交互。所以这就是我们的屏幕可能的样子。我们还包括了可能需要的其他组合状态,所以它们被提升到了屏幕外部。

@Composable
fun PlacesScreen(
    state: PlacesState,
    pagerState: PagerState,
    onFavoritesButtonClick: (Place) -> Unit,
    onNavigateToPlaceButtonClick: (Place) -> Unit
) {
    Scaffold {
        PlacesPager(
          pagerState = pagerState,
          state = state,
          onFavoritesButtonClick = onFavoritesButtonClick,
          onNavigateToPlaceButtonClick = onNavigateToPlaceButtonClick
        )
    }
}
@Composable
fun PlacesRoute(
    navController: NavController,
    viewModel: PlacesViewModel = hiltViewModel(),
) {
    // ... state collection

    LaunchedEffect(state.error) {
        state.error?.let {
            context.showError()
            viewModel.dismissError()
        }
    }

    PlacesScreen(
        state = uiState,
        onFavoritesButtonClick = //..
        onNavigateToPlaceClick = {
            when {
                permissionState.isGranted -> {
                    analyitcs.track("StartRoutePlanner")
                    navController.navigate("RoutePlanner")
                }
                permissionState.shouldShowRationale -> {
                     analytics.track("RationaleShown")
                     navController.navigate("LocationRationale")
                }
                else -> {
                    permissionState.launchPermissionRequest()
                }
            }
        }
    )
}

Route

路由(Route)是整个流程的入口。

@Composable
fun PlacesRoute(
    navController: NavController,
    viewModel: PlacesViewModel = hiltViewModel(),
) {
    // ... state collection

    LaunchedEffect(state.error) {
        state.error?.let {
            context.showError()
            viewModel.dismissError()
        }
    }

    PlacesScreen(
        state = uiState,
        onFavoritesButtonClick = //..
        onNavigateToPlaceClick = {
            when {
                permissionState.isGranted -> {
                    analyitcs.track("StartRoutePlanner")
                    navController.navigate("RoutePlanner")
                }
                permissionState.shouldShowRationale -> {
                     analytics.track("RationaleShown")
                     navController.navigate("LocationRationale")
                }
                else -> {
                    permissionState.launchPermissionRequest()
                }
            }
        }
    )
}

这是PlacesRoute函数的简化版本,但已经相当庞大。随着每个新的用户交互和基于状态的效果,这个函数的大小将会增长,使其变得更难理解和维护。另一个问题是回调函数。随着每个新的用户交互,我们将不得不在PlacesScreen的声明中添加另一个回调,这也可能会变得相当大。

另外,让我们考虑一下测试。我们可以轻松测试屏幕和ViewModel,但是Route呢?它有很多内容,不是每样东西都可以轻松模拟。首先,它与屏幕耦合在一起,所以如果没有引用它,我们将无法适当地进行单元测试。将其他组件替换为存根将需要我们将所有内容移到Route的声明中。

进行改变

让我们尝试解决我们迄今为止已经确定的这些问题

Action

在看到这些回调时,我脑海中首先想到的是如何将它们进行分组。而我当时所做的第一件事情是这样的:

sealed interface PlacesAction {
    data class NavigateToButtonClicked(val place: Place) : ParcelAction
    data class FavoritesButtonClicked(val place: Place) : ParcelAction
}

虽然这使我们能够将我们的操作分组到一个明确定义的结构中,但也带来了不同的问题。

在屏幕级别上,我们将不得不实例化这些类并调用我们的onAction回调。如果你熟悉重组(Re-composition)的工作原理,当涉及到lambda表达式时,你可能还会有冲动将其包裹在remember中,以避免不必要的UI重新渲染。

@Composable
fun PlacesScreen(
    state: PlacesState,
    onAction: (PlacesAction) -> Unit
) {
    PlacesPager(
        onFavoritesButtonClicked = { onAction(PlacesAction.FavoritesButtonClicked(it))}
    )
}

另一方面,Route还引入了另一件我不太喜欢的事情——可能是巨大的when语句。

PlacesScreen(
        state = uiState,
        onAction = { when(it) {
        FavoritesButtonClick = //..
        NavigateToPlaceClicked = {
            when {
                permissionState.isGranted -> {
                    analyitcs.track("StartRoutePlanner")
                    navController.navigate("RoutePlanner")
                }
                permissionState.shouldShowRationale -> {
                     analytics.track("RationaleShown")
                     navController.navigate("LocationRationale")
                }
                else -> {
                    permissionState.launchPermissionRequest()
                }
            }
        }
    )

所有这些都使我找到了一个更好的解决方案,那就是一个简单的数据类。

data class ParcelActions(
    val onFavoritesClicked: (Place) -> Unit = {},
    val onNavigateToButtonClicked: (Place) -> Unit = {},
)

这使我们能够在与屏幕相关的操作中引入相同的分组水平和便利性,以及一种更简单的方式将这些操作传递给相关组件。

@Composable
fun PlacesScreen(
    state: PlacesState,
    actions: PlacesActions
) {
    PlacesPager(
        onFavoritesButtonClicked = actions.onFavoritesButtonClicked,
        onNavigateToPlaceButtonClicked = actions.onNavigateToPlaceButtonClicked
    )
}

现在,在Route方面,我们还可以避免使用when语句,并引入以下实用程序,以便在每次重组时不会重新创建Actions类,使Route更加简洁。

@Composable
fun PlacesRoute(
    viewModel: PlacesViewModel,
    navController: NavController,
) {

    val uiState by viewModel.stateFlow.collectAsState()
   
    val actions = rememberPlacesActions(navController)


    LaunchedEffect(state.error) {
        state.error?.let {
            context.showError()
            viewModel.dismissError()
        }
    }

    PlacesScreen(
        state = uiState,
        actions = actions
    )

}

@Composable
fun rememberPlacesActions(
    navController: NavController,
    analytics: Analytics = LocalAnalytics.current,
    permissionState: PermissionState = rememberPermissionState(),
) : PlacesActions {
    return remember(permissionState, navController, analytics) {
        PlacesActsions(
            onNavigateToPlaceClick = {
            when {
                permissionState.isGranted -> {
                    analyitcs.track("RoutePlannerClicked")
                    navController.navigate("RoutePlanner")
                }
                permissionState.shouldShowRationale -> {
                     analytics.track("RationaleShown")
                     navController.navigate("LocationRationale")
                }
                else -> {
                    permissionState.launchPermissionRequest()
                }
            }
        }
        )
    }   
}

虽然PlacesRoute现在更加直观,但我们所做的只是将其所有的Actions逻辑移到另一个函数中,这既没有提高可读性,也没有提高可扩展性。此外,我们的第二个问题仍然存在——基于状态的效果。我们的UI逻辑现在也分散开来,引入了不一致性,并且我们并没有使其变得更具可测试性。现在是我们引入最后一个组件的时候了。

Coordinator

协调器的核心作用,正如你可能从其名称中猜到的,是协调不同的操作处理程序和状态提供者。协调器观察和响应状态变化,并处理用户操作。你可以将其视为我们流程的Compose状态。在我们简化的示例中,协调器的样子如下。
需要注意的是,由于我们的协调器现在不在可组合范围内,我们可以以更直接的方式处理一切,无需LaunchedEffect,就像我们通常在ViewModel中所做的那样,只不过这里的业务逻辑是UI逻辑。

class PlacesCoordinator(
    val viewModel: PlacesViewModel,
    val navController: NavController,
    val context: Context,
    private val permissionState: PermissionState,
    private val scope: CoroutineScope
) {

    val stateFlow = viewModel.stateFlow

    init {
        // now we can observe our state and react to it
        stateFlow.errorFlow
            .onEach { error ->
                context.toast(error.message)
                viewModel.dismissError()
            }.launchIn(scope)
    }

    // and handle actions
    fun navigateToRoutePlanner() {
        when {
            permissionState.isGranted -> {
                viewModel.trackRoutePlannerEvent()
                navController.navigate("RoutePlanner")
            }
            permissionState.shouldShowRationale -> {
                viewModel.trackRationaleEvent()
                navController.navigate("LocationRationale")
            }
            else -> permissionState.launchPermissionRequest()
        }
    }

}

我们的Action将修改成如下

@Composable
fun rememberPlacesActions(
   coordinator: PlacesCoordinator
) : PlacesActions {
    return remember(coordinator: PlacesCoordinator) {
        PlacesActsions(
            onFavoritesButtonClicked = coordinator.viewModel::toggleFavorites,
            onNavigateToPlaceButtonClicked = coordinator::navigateToRoutePlanner
        )
}

我们的Route修改如下

@Composable
fun PlacesRoute(
    coordinator: PlacesCoordinator = rememberPlacesCoordinator()
) {

    val uiState by coordinator.stateFlow.collectAsState()
   
    val actions = rememberPlacesActions(coordinator)

    PlacesScreen(
        state = uiState,
        actions = actions
    )

}

在我们的示例中,PlacesCoordinator 现在负责在我们的功能流中发生的UI逻辑。由于它了解不同的状态,我们可以轻松地对状态变化做出反应,并为每个用户交互构建条件逻辑。如果交互很直接,我们可以轻松地将其委托给相关的组件,比如 ViewModel。
通过拥有协调器,我们还可以控制向屏幕公开哪些状态。如果我们有多个 ViewModel 或者 ViewModel 状态对于我们正在处理的屏幕来说过于庞大,我们可以将这些状态组合起来或者公开部分状态。

val screenStateFlow = viewModel.stateFlow.map { PartialScreenState() }
    // or
    val screenStateFlow = combine(vm1.stateFlow, vm2.stateFlow) { ScreenStateFlow() }

另一个好处是,整个流程的UI逻辑现在与Route解耦,这意味着我们可以将我们的Coordinator作为另一个Route的一部分使用,而无需复制重要内容并保持屏幕部分无状态。

@Composable
fun TwoPanePlacesRoute(
    detailsCoordinator: PlacesDetailsCoordinator,
    placesCoordinator: PlacesCoordinator
) {
    
    TwoPane(
        first = {
            PlacesScreen(
                state = placesCoordinator.state,
                actions = rememberPlacesActions(placesCoordinator)
            )
        },
        second = {
            PlaceDetailsScreen(
                state = detailsCoordinator. state,
                actions = rememberDetailsActions(detailsCoordinator)
            )
        }
    )
}

最后,现在我们可以通过测试实现它的组件来测试我们的UI逻辑。让我们看看如何通过使用我们的“当权限被拒绝时导航到理由屏幕”来测试我们的Coordinator。
这部分假设您对如何测试Composable组件有一些了解。

fun test_NavigateToRatinoleIfPermissionWasDeniedBefore() {
     composeRule.setContent {
            // 1
            ComposableUnderTest(
                coordinator = rememberPlacesCoordinator(
                    navController = testNavController,
                    viewModel = NearbyPlacesViewModel()
                )
            )
        }
        
        // 2
        composeRule.onNode(hasText("Navigate")).performClick()

        // 3
        Assert.assertEquals(
            "locationRationale",
            navController.currentBackStackEntry?.destination?.route
        )
}

让我们快速浏览一下这个测试:

  1. 首先,我们发出了作为测试对象的Composable UI。这个UI的结构很简单,直接调用了我们的Coordinator。
 @Composable
private fun ComposableUnderTest(coordinator: NearbyPlacesCoordinator) {
    NavHost(
        navController = coordinator.navController,
        startDestination = "home"
    ) {
        composable("home") {
            Button(onClick = { coordinator.navigateToPlace(Place.Mock) }) {
                Text(text = "Navigate")                
            }
         }
        composable("locationRationale") {
            Text(text = "No permission")
        }
    }
}

  1. 其次,我们以编程方式点击“导航”按钮,触发操作并让Coordinator处理它。
  2. 最后,我们通过检查NavHostController中的当前目标是否与我们预期的目标一致来验证我们的假设是否有效,我们的实现是否正常工作。

总结一下我们进行的重构和取得的成就:

  1. 我们的Screen仍然完全是无状态的。它仅依赖于作为函数参数传递的内容。所有用户交互都通过Actions暴露给其他组件处理。
  2. Route现在在导航图中充当简单的入口点。它收集状态,在重新组合过程中记住我们的操作。
  3. Coordinator现在正在做大部分繁重的工作:响应状态变化并将用户交互委派给其他相关组件。它完全与Screen和Route解耦,可以在另一个路由中重用并单独测试。
    以下图表展示了我们现在拥有的数据流程。

问答时间

每个Compose屏幕都需要一个协调器吗?
简短的回答是:这取决于情况!对于一个非常简单的流程,比如一个带有两个操作的对话框,可能有点过于复杂。你可能会完全取消操作数据类,将这些操作放在路由中处理。对于一个随着时间复杂度增加的屏幕,我认为从一开始就值得投资,或者在看到路由增长时开始进行重构。

LaunchedEffect 是否已被“弃用”?
当然没有!同样,一个没有协调器的简单屏幕可以使用LaunchedEffect来对状态变化做出反应,这是完全可以的。当UI逻辑存在于屏幕层并在屏幕层中终止时,您仍然可以在屏幕中使用LaunchedEffect,例如动画。

路由没有做太多事情
是的,在我们的示例中,路由在责任方面相当轻量级。但将其作为导航入口意味着更多。许多不是基于状态的效果都属于路由的处理范畴。例如,我们可以使用SideEffect来调整颜色,或者放置BackHandler来拦截返回按钮的按下,这在屏幕内并不总是合适。

协调器会像路由一样随着时间而增长吗?
很可能是的。这可能是它正在做太多事情的迹象,其中一些事情可以提取到另一个具有状态的组件中,甚至是另一个协调器中。就像您从屏幕中提取不同的UI组件来封装一些UI一样,您可以构建其他组件或协调器来封装UI逻辑。

资源

Jetpack Compose UI Architecture IDE Plugin:https://plugins.jetbrains.com/plugin/19034-jetpack-compose-ui-architecture-templates
compose ui架构文档:https://levinzonr.github.io/compose-ui-arch-docs/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/921325.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

margnalizeHuberJacibian测试Demo

文章目录 margnalize公式及原理&#xff1a;测试代码及运行结果解说代码编译命令&#xff1a;运行结果&#xff1a; Huber原理代码 Jacibian测试代码代码解释代码编译命令运行结果 margnalize 公式及原理&#xff1a; 测试代码及运行结果 解说 /***************************…

python 包管理工具poetry和异步sanic web框架实践+配置镜像源

上传源码到服务器&#xff0c;然后执行poetry install安装依赖。 多进程启动脚本run.sh内容&#xff1a; #!/bin/bash #应用入口文件 APP_NAME/www/wwwroot/python-sanic/main.py #进程关键字 PROCESS_KEYWORDpython-sanic #使用说明&#xff0c;用来提示输入参数 usage(){…

潮玩数藏App:数字时代下的潮流收藏新体验

随着数字时代的到来&#xff0c;潮流收藏成为了一种新型的文化现象&#xff0c;并在年轻人中迅速流行起来。为了满足这一需求&#xff0c;潮玩数藏App应运而生&#xff0c;为用户提供了一个专业、便捷的潮流收藏平台。本文将深入探讨潮玩数藏App的专业性、思考深度以及逻辑性&a…

“爱在七夕,情暖人间”店口志愿者开展敬老助残服务活动

在这个充满爱心的世界里&#xff0c;让志愿服务燃烧我们的人生。 8月22日七夕节&#xff0c;诸暨市爱心助残协会联合牛皋社区党支部、老年协会、店口镇义工协会、店口镇残疾人之家、诸暨凯客蛋糕、诸暨中康医院、杨琼发艺工作室、国芬理发店等爱心团队在店口残疾人之家开展了“…

好用的电容笔有哪些推荐?开学季便宜好用电容笔推荐

开学马上要来了&#xff0c;想必很多学生党都在为开学而做准备&#xff0c;要知道&#xff0c;原装的Apple Pencil&#xff0c;虽然功能很强&#xff0c;但是价格却很贵&#xff0c;不是一般人能够承受得起的。所以&#xff0c;是否也有类似于Apple Pencil这样的电容笔&#xf…

直播app源码,会话描述协议SDP:高质量平台

摘要&#xff1a; SDP协议又称为会话描述协议&#xff0c;在直播app源码平台中&#xff0c;通过定义实时通信参数&#xff0c;管理会话信息和媒体数据&#xff0c;来为用户提供实时通信服务&#xff0c;确保通信的质量与稳定&#xff0c;例如:在直播app源码平台的直播间中&…

每年节约3千万!微信实验平台Iceberg湖仓一体架构改造

# 关注并星标腾讯云开发者 # 每周3 | 谈谈我在腾讯的架构设计经验 # 第4期 | 黄延岩&#xff1a;微信实验平台 - 全面拥抱湖仓时代 微信实验平台简介 微信实验平台主要提供微信内部各个业务场景&#xff08;视频号、直播、搜一搜、公众号等&#xff09;下的各类实验场景的支持&…

苍穹外卖 day1 搭建成功环境

引入 idea找不到打包生成的文件目录怎么办&#xff0c;首先点击这个小齿轮 show ecluded files然后就能找到隐藏的文件 这个jar包内含tomcat&#xff0c;可以直接丢在linux上用 开发环境&#xff1a;开发人员在开发阶段使用的环境&#xff0c;一般外部用户无法访问 测试环…

清华源的链接太多老崩溃,我把它拷过来,需要什么点什么

建议按照字母分个类可能会好点 把链接这里改为 哈哈就不卡了&#xff0c;浏览器也不崩溃了还能很快就链接成功 Links for pandas这是链接 这个小技巧教给大家请给我点个赞

uniapp 实现切换tab锚点定位到指定位置

1.主要使用uniapp scroll-view 组件的scroll-into-view属性实现功能 2.代码如下 <scroll-view:scroll-into-view"intoView"><u-tabsclass"tabs-list"change"tabChange":list"tabList"></u-tabs><view id"1&…

Wireshark流量分析例题

目录 前言 一、题目一(1.pcap) 二、题目二(2.pcap) 三、题目三(3.pcap) 四、题目四(4.pcap) 前言 Wireshark流量包分析对于安全来说是很重要的&#xff0c;我们可以通过Wireshark来诊断网络问题&#xff0c;检测网络攻击、监控网络流量以及捕获恶意软件等等 接下来我们…

pyqt5-自定义停靠栏头部

import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import *class CustomDock(QDockWidget):def __init__(self, title: str, parentNone):super().__init__(title, parent)"""停靠栏的头部"""h_layout Q…

柔性数组详解

柔性数组 1.前言 在c99标准中&#xff1a;允许结构体的最后一个变量是未知大小的数组&#xff0c;这就是柔性数组的来源。 例如&#xff1a; typedef struct type_a{ int i;int a[0];//柔性数组成员 }type_a;有些编译器可能会报错&#xff0c;那就使用下面这一种定义方式&…

数据驱动洞察:各种词频分析技术挖掘热点数据

一、引言 随着信息时代的发展&#xff0c;人们的关注点日益复杂多样。社交媒体、新闻网站和论坛等平台上涌现了大量的信息&#xff0c;这使得热点分析成为了解社会热点话题和舆情动向的重要手段。词频统计是热点分析的基础&#xff0c;本文将分别介绍基于ElasticSearch、基于S…

C++动态规划经典案例解析之合并石子

1. 前言 区间类型问题&#xff0c;指求一个数列中某一段区间的值&#xff0c;包括求和、最值等简单或复杂问题。此类问题也适用于动态规划思想。 如前缀和就是极简单的区间问题。如有如下数组&#xff1a; int nums[]{3,1,7,9,12,78,32,5,10,11,21,32,45,22}现给定区间信息[…

中国区域地表净辐射数据集(1982-2017)

摘要 地表净辐射(Rn) 是陆地生态系统水、热、碳循环过程中的重要参数,准确地估算Rn并分析其时空变化特征对气候变化和能量平衡等研究具有重要意义。本文基于CMFD再分析资料和GLASS地表反照率数据集估算1982~2017年中国区域逐日Rn。本数据集将逐日Rn统计为每月Rn,空间分辨率…

诚迈科技携数智成果亮相南京软博会,交出数实融合圆满答卷

8月20-23日&#xff0c;南京软博会在南京国际博览中心盛大举行。诚迈科技聚焦数字中国建设需求和万物智联时代关键软件技术发展趋势&#xff0c;全方位展示了一系列操作系统产品和数智化解决方案成果&#xff0c;旨在推动数实融合创新。此外&#xff0c;诚迈科技受邀出席中国工…

【业务功能篇74】三高微服务项目springboot-springcloud

三高指的是&#xff1a;高性能、高并发、高可用 2.项目架构 2.1 系统架构图 整体的项目架构图如下 2.2 业务组成 整体的项目业务组成如下

Elasticsearch简介及安装

&#x1f353; 简介&#xff1a;java系列技术分享(&#x1f449;持续更新中…&#x1f525;) &#x1f353; 初衷:一起学习、一起进步、坚持不懈 &#x1f353; 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正&#x1f64f; &#x1f353; 希望这篇文章对你有所帮助,欢…

指针与引用详解

博文内容&#xff1a; 指针与引用 指针和引用的区别&#xff1f; 这个问题主要还是仁者见仁&#xff0c;智者见智。 主要就是对于俩者的出现时间来看&#xff0c;对于指针来说&#xff0c;早在C语言那会就已经出现&#xff0c;C是后于C语言的&#xff0c;所以C的出现会多少在使…