ASP.NET MVC ViewBag详解:原理、选型与避坑指南
发布时间:2026/10/6 5:20:11 作者:尧图编辑部 阅读量:1,286

1. 先弄明白 ViewBag 到底是什么它没有你想象的那么玄乎1.1 一句话本质它是 ViewData 的动态马甲当年我第一次从 Web Forms 转到 ASP.NET MVC最不习惯的就是往视图传数据这件事。Web Forms 里拖个控件、绑定数据源就行到了 MVC 一切都要自己动手。于是那段时间我的 Controller 里全是ViewData[xxx]和ViewBag.xxx来回混用用了一阵子才真正把这俩东西看透。ViewBag 本质上是ControllerBase的ViewBag属性类型是dynamic它包装的是一个ViewDataDictionary字典对象。换句话说当你写ViewBag.Title 首页时编译器什么都不查运行时它做的其实是往ViewData[Title]里塞了一个值。这是 ViewBag 最核心的秘密它不是独立的存储容器而是 ViewData 字典的一个动态访问入口。怎么验证这一点很简单。在 Controller 里写public ActionResult Index() { ViewBag.Name 张三; return View(); }然后在视图里同时输出这两个东西pViewBag.Name/p pViewData[Name]/p你会看到两行都显示张三。反过来也一样你用ViewData[Name]赋值用ViewBag.Name取值结果相同。因为它们操作的是同一个底层字典只是写法不同罢了。那微软为什么要搞两个东西出来历史原因是 C# 4.0 引入了dynamic关键字ASP.NET MVC 3 顺势提供了dynamic类型的ViewBag属性让开发者可以用属性访问语法ViewBag.Foo而不是字典索引语法ViewData[Foo]。刚出的那几年大家觉得这写法又短又顺手比 ViewData 那一堆中括号清爽多了于是铺天盖地都是 ViewBag。1.2 dynamic 类型意味着什么理解 ViewBag 的关键在于理解dynamic到底带来了什么。普通 C# 代码里obj.Name这种调用是在编译期就绑定好方法的IDE 有智能提示拼错了直接编译报错。而dynamic类型恰恰相反编译期它不绑定任何东西编译器只负责在运行时发出一个动态绑定请求让 CLR 通过反射去找这个属性到底存不存在。这就带来两个直接后果。第一代码写起来非常自由。你可以随便给 ViewBag 定义任意属性名比如ViewBag.SiteName、ViewBag.PageSize、ViewBag.MyCustomData只要赋值时不重名都不会有任何编译问题。这个特性用来传零散的页面数据确实方便。第二错误全部推迟到运行时。ViewBag.Tilte拼错不会在编译时报错只会在页面渲染那一刻抛RuntimeBinderException或者更麻烦的情况——不抛异常直接给你输出一个空字符串让你排查半天想不明白。我经常跟团队新人说一句话ViewBag 是一把没有保险的枪用着顺手但走火的时候你才知道疼。2. 入门三板斧Controller 传值、View 取值、循环渲染2.1 Controller 里最基础的赋值写法先来一个最典型的入门场景。假设你有个新闻首页需要把页面标题、公告、当前登录用户传给视图public ActionResult Index() { ViewBag.PageTitle 某某资讯 - 首页; ViewBag.Notice 端午假期平台暂停发货通知; ViewBag.LoginName User.Identity.IsAuthenticated ? User.Identity.Name : 游客; return View(); }这里有几个细节值得说。第一ViewBag 属性名的命名全凭约定没有强制规则。但实战中最好统一风格项目里如果一半人写PageTitle一半人写pageTitle时间一长准出事。我所在的项目组后来强制规定ViewBag 键名一律用 PascalCase视图里取用必须完全一致代码评审阶段重点查这个。第二ViewBag 可以存任意引用类型包括 List、匿名对象、复杂实体。比如从数据库取一列商品public ActionResult Products() { ListProduct products; using (var db new AppDbContext()) { products db.Products.Where(p p.IsOnSale).Take(12).ToList(); } ViewBag.Products products; ViewBag.TotalCount products.Count; return View(); }注意这里我特意在外面声明了ListProduct再赋值而没有直接写ViewBag.Products db.Products...ToList()。这不是矫情是为了让代码在调试时更容易观察类型也避免后续在视图中做复杂操作时出现类型转换问题。2.2 Razor 视图里的三种取值姿势在 .cshtml 视图里取值姿势基本有三类直接输出、赋值给局部变量、参与表达式运算。最直接的输出h1ViewBag.PageTitle/h1 div classnoticeViewBag.Notice/div如果你要把值暂存起来复用可以用{ }代码块{ var loginName ViewBag.LoginName as string ?? 游客; var totalCount (int)ViewBag.TotalCount; } p当前用户loginName/p p商品总数totalCount/p这里我用了as string和(int)显式转换原因后面避坑章节会详细讲。现在你只需要记住ViewBag 取出来的值是 object / dynamic直接使用前一定要想清楚它到底是什么类型。还有一个常见的运算符坑。假设你想根据 ViewBag 里的布尔值或数字做判断if (ViewBag.IsVip true) { spanVIP 会员/span }这段没问题。但如果是这种(ViewBag.TotalCount 0 ? 暂无商品 : 有商品)必须加括号。如果你漏了括号写成ViewBag.TotalCount 0 ? 暂无商品 : 有商品Razor 解析器会把ViewBag.TotalCount先输出然后再比较和三元表达式结果页面上出现一串12 0 ? 暂无商品 : 有商品这种怪东西。这个细节我见过不止一个新手栽进去。2.3 传列表数据并渲染的完整案例把列表数据用 ViewBag 传到视图后最常见的渲染方式是 foreach 循环h2在售商品/h2 { var productList ViewBag.Products as ListProduct; } if (productList ! null productList.Any()) { ul foreach (var product in productList) { li strongproduct.Name/strong span¥product.Price/span /li } /ul } else { p暂时没有在售商品/p }注意我做了三件事as ListProduct强转、判空、判 Any。这三步少一步都可能出问题——如果 Controller 里因为某种原因没给ViewBag.Products赋值视图直接foreach一个 null 就会抛异常页面直接白屏。这种Controller 忘赋值的 bug 在 ViewBag 场景下非常难查因为你编译不报错只能运行时看。如果你用的商品对象是匿名类型比如ViewBag.Products products.Select(p new { p.Name, p.Price }).ToList();那么在 foreach 里依然可以访问product.Name、product.Price因为 dynamic 绑定的对象是真实存在的。但注意匿名类型不能跨程序集访问得是同一个程序集生成的对象才能动态访问其属性这个小知识后面迁移章节还会提到。3. 同门四兄弟怎么选ViewBag、ViewData、TempData、ViewModel 的对比与选型标准3.1 四兄弟的底层差异ASP.NET MVC 里往视图传数据官方给了你四个兄弟但很多新手搞不清它们各自的分工。我直接放一张表方式底层存储生命周期类型安全典型场景ViewDataViewDataDictionary当前请求内弱类型需拆箱传少量零散数据ViewBag动态包装 ViewData当前请求内弱类型无编译期检查传少量零散数据语法更简洁TempData默认基于 Session跨一次 Redirect弱类型PRG 模式Post-Redirect-Get传提示信息ViewModel强类型 Model 对象当前请求内强类型页面主体数据推荐首选ViewData 和 ViewBag 的生命周期都只限于当前请求。请求结束这些数据就被回收了。如果你在 Controller 里赋值后RedirectToAction跳到另一个 Action新的请求里 ViewBag 是全新的、空的什么都不会有。这是大家最容易踩的一个概念坑我会在避坑章节展开。TempData 则不一样。它底层走的是 SessionASP.NET Core 中默认改为 Cookie 存储因此可以跨一次重定向存活。它有一套读一次就标记删除的机制第一次读取后如果不在同一请求内再次访问第二次请求时数据就会被清掉。想保留数据继续读得用TempData.Keep()或TempData.Peek()。这套机制设计的初衷是防止 PRG 模式下刷新页面时重复提交表单所以它天然适合传操作成功、保存失败之类的提示消息。ViewModel 我放到下面单独讲。3.2 类型安全与重构成本这是四兄弟里最本质的分水岭类型安全。ViewData / ViewBag / TempData 都是弱类型。你存进去的是 object取出来时如果类型不对要么抛异常要么静默出错。更重要的是你改属性名的时候编译器帮不了你。比如你用ViewBag.UserName后来想统一改成ViewBag.DisplayName你得全局搜索替换。如果不小心漏掉了一个视图运行时才发现而且是在用户打开那个页面时才发现。ViewModel 是强类型的public class ProductListViewModel { public string PageTitle { get; set; } public ListProduct Products { get; set; } public int TotalCount { get; set; } public bool IsVip { get; set; } }Controller 里构造好这个对象return View(vm)视图顶部声明model ProductListViewModel然后直接用Model.PageTitle、Model.Products。这种情况下任何一个属性名拼错编译器立刻报错IDE 重命名也能全链路同步。重构成本大幅降低。从团队协作角度看ViewModel 还带来一个额外好处数据结构文档化。新人看一眼 ViewModel 类就知道这个页面需要哪些数据而不是去 Controller 里翻各种 ViewBag.xxx。3.3 我的选型标准项目里用多了之后我给自己定了一套选型标准分享出来供参考页面主体数据列表、表单、详情页等一律强类型 ViewModel。这没什么好商量的。布局页公共数据站点名、当前用户、菜单列表用 ActionFilter 统一塞 ViewBag后面专门讲。PRG 场景的提示信息用 TempData别用 ViewBag因为跨请求 ViewBag 根本传不过去。Controller 内部给视图传一两个临时参数比如当前选中的 Tab、搜索关键字回显可以用 ViewBag图个方便。禁止把复杂业务对象塞 ViewBag。一旦涉及循环逻辑判断、多层级页面展示一律建模。这套标准在多数企业级项目里都适用。核心原则就一句话ViewBag 适合穿针引线的临时数据不适合承载一个页面的主体业务。4. 避坑专题这些年我们为 ViewBag 踩过的那些坑4.1 坑一空引用和 null 处理这是 ViewBag 第一大坑。ViewBag 的任何一个未赋值属性读取时返回的都是 null而且不报错。比如你写了if (ViewBag.UserRole admin) { span管理员/span }Controller 里忘了给ViewBag.UserRole赋值这段不会报错页面什么都不显示这只是轻度症状。真正头痛的是下面这种ViewBag.User.Profile.Name只要 Controller 里某个环节没给ViewBag.User赋值或者赋的是 null页面直接白屏抛Microsoft.CSharp.RuntimeBinderException而且异常信息里未必能一眼看清是哪个属性为空。我的经验是视图里用到 ViewBag 的地方一律做防御处理。要么在 Controller 里保证一定赋值包括赋 null 的兜底要么视图里用空合并运算符兜底(ViewBag.Notice ?? 暂无公告)条件判断的地方尽量先取到局部变量再判断{ var userRole ViewBag.UserRole as string; } if (string.Equals(userRole, admin, StringComparison.OrdinalIgnoreCase)) { span管理员/span }这样至少能避免属性不存在导致的运行时异常和类型不匹配导致的奇怪行为混在一起。4.2 坑二ViewData 和 ViewBag 互相覆盖前面说过 ViewBag 底层就是 ViewData所以这两个东西共用同一个键空间。这意味着ViewBag.Title 第一个标题; ViewData[Title] 第二个标题;最终Title的值是第二个标题。反过来ViewData[Title] 第一个标题; ViewBag.Title 第二个标题;最终值是第二个标题。规则只有一条同一键名下后写的赢。这本身不叫 bug真正埋雷的是团队混用。你在这个页面用 ViewBag.Title 赋值同事在布局页用 ViewData[Title] 取值两边以为各管各的实际在争同一个槽位改完这个改那个越改越乱。对付这个坑的办法项目里定一条不成文的规矩要么全用 ViewBag要么全用 ViewData同一个键名不要两种语法混写。布局页和子页面之间传递Title这类公共键名尤其要统一约定。4.3 坑三大小写和拼写错误只在运行时爆炸ViewBag 的属性名是大小写敏感的而且完全不经过编译器。ViewBag.UserName和ViewBag.username是两个不同的键。这大概是 ViewBag 场景里出现频率最高的线上事故类型了。我曾经帮同事排查过一个线上 bug后台编辑页面一直显示用户名不存在刷新十次都一样。查了很久最后发现是 Controller 里写的是ViewBag.UserName user.Name;而视图里写的是ViewBag.UserName.ToUpper()两边差一个字母——视图少写了一个字母 n。编译全过测试环境没测到上线就炸。还有一个延伸坑属性名不能随便和 C# 关键字撞车。比如你写ViewBag.Class boxclass是关键字视图里ViewBag.class这种写法非常别扭而且容易出错。同理ViewBag.Namespace、ViewBag.Event这类名字看着正常但在 Razor 里也可能引发语法解析问题。稳妥的做法是遇到这类名字要么改名比如ViewBag.CssClass要么改用ViewData[class]。4.4 坑四Redirect 之后 ViewBag 还在吗答案很干脆不在。ViewBag 是请求级数据一次 HTTP 请求结束整个 ViewData 字典就没了。这是个非常经典的操作失误。你写[HttpPost] public ActionResult Save(FormData form) { // 保存逻辑... ViewBag.Message 保存成功; return RedirectToAction(List); }然后ListAction 的视图里写ViewBag.Message你会得到一个空字符串。原因就是RedirectToAction会触发浏览器发起一个全新的 GET 请求这个新请求里 Controller 是全新的实例ViewBag 自然是空的。正确做法是用 TempData[HttpPost] public ActionResult Save(FormData form) { TempData[Message] 保存成功; return RedirectToAction(List); }List 视图里if (TempData[Message] ! null) { div classalert alert-successTempData[Message]/div }TempData 跨一次请求存活正好覆盖 PRG 场景。这是 MVC 开发者必须记住的分工同请求内用 ViewBag/ViewData跨请求用 TempData。4.5 坑五高并发或异步场景下的串味问题有些同学担心 ViewBag 是不是静态的会不会在并发请求下互相覆盖。这个可以放心ViewBag 是 Controller 实例属性而 ASP.NET 每个请求都会创建新的 Controller 实例所以正常情况下不存在跨请求串数据。真正的问题出在异步场景。假如你在 Action 里这样写public async TaskActionResult Detail(int id) { ViewBag.Data await _service.GetDataAsync(id); return View(); }这没问题。但如果有人在异步回调里给 ViewBag 复制或者更常见的在多个 async 任务并行执行后再统一赋值你要注意 ViewBag 的赋值顺序。Task.WhenAll内部的多个异步操作如果都尝试写 ViewBag 的同一键名最后谁能写入完全取决于谁先完成这种不确定性会带来偶发性 bug。我的建议是ViewBag 只在 Action 方法体内部同步赋值所有异步结果先落到局部变量再统一赋给 ViewBag。不要在 lambda、async 回调、并行任务里直接操作 ViewBag。另一个容易被忽视的点是ViewBag 里的数据默认是 object视图渲染时如果要遍历大量数据dynamic 绑定的性能开销虽然微乎其微但大型页面里频繁访问 ViewBag 属性每次都会走运行时反射绑定。性能敏感的项目里页面主体数据还是走强类型 ViewModelViewBag 只放零星配置项。5. 把 ViewBag 用出体系感ActionFilter 统一初始化页面数据的实战套路5.1 什么时候需要 ActionFilter 来喂 ViewBag先描述一个常见痛点。你的网站布局页Layout需要显示这些东西站点名称、当前登录用户、主导航菜单、全局通知条。传统写法是在每个 Action 里手动赋值public ActionResult Index() { ViewBag.SiteName 某某平台; ViewBag.CurrentUser GetCurrentUser(); ViewBag.Menu GetMenu(); return View(); } public ActionResult About() { ViewBag.SiteName 某某平台; ViewBag.CurrentUser GetCurrentUser(); ViewBag.Menu GetMenu(); return View(); }Action 一多这些重复代码变得非常恶心而且万一某个 Action 忘了赋值布局页对应区域就静默空白你还得挨个 Action 排查。这个时候就该上ActionFilter。ActionFilter 是 MVC 管道里的一个拦截器可以在 Action 执行前后插入逻辑。OnActionExecuting在 Action 方法体执行之前触发这时候给 ViewBag 赋值Action 执行完渲染视图时正好能用到。所有经过这个 Filter 的 Action都能自动拿到公共数据完美解决每个 Action 都写一遍的重复劳动。5.2 MVC 里的 ActionFilterAttribute 完整示例在经典 ASP.NET MVC 5 里Filter 命名空间是System.Web.Mvc写法如下using System.Web.Mvc; public class LayoutDataFilter : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { var httpContext filterContext.HttpContext; var controller filterContext.Controller; controller.ViewBag.SiteName 某某平台; controller.ViewBag.CurrentUser httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.Name : null; controller.ViewBag.MenuItems new Liststring { 首页, 产品, 关于我们, 帮助中心 }; base.OnActionExecuting(filterContext); } }注册方式有两种。全局注册在Global.asax.cs的Application_Start里public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); GlobalFilters.Filters.Add(new LayoutDataFilter()); } }或者只让某个控制器及其 Action 生效[LayoutDataFilter] public class HomeController : Controller { // 这里的每个 Action 都能自动拿到 ViewBag.SiteName 等公共数据 }注意一个顺序问题如果你用了多个 FilterOnActionExecuting的执行顺序是注册顺序。全局 Filter 先执行Controller 上挂的 Filter 次之Action 上挂的 Filter 最后。如果你的某个 Action 想覆盖全局 Filter 里设置的ViewBag.SiteName直接在 Action 方法体里重新赋值即可因为 Action 执行晚于所有 Filter 的OnActionExecuting。5.3 别搞混System.Web.Http.Filters.ActionFilterAttribute 是另一回事写到这里必须强调一个很多人在网上搜教程时踩过的坑搜索 ActionFilter 时会搜出来一个叫System.Web.Http.Filters.ActionFilterAttribute的类它跟 ViewBag 一点关系都没有。System.Web.Http是 ASP.NET Web API 的命名空间。Web API 的 ApiController 面向的是 REST 接口返回 JSON/XML根本没有视图这个概念自然也没有 ViewBag。两个ActionFilterAttribute同名但完全独立一个是 MVC 专属一个是 Web API 专属。一旦你在项目里同时引用了System.Web.Mvc和System.Web.Http命名空间这种项目不少MVC 和 Web API 共存很常见写ActionFilterAttribute时编译器会提示二义性。解决的办法是写全限定名或起别名using MvcFilter System.Web.Mvc.ActionFilterAttribute; using ApiFilter System.Web.Http.Filters.ActionFilterAttribute;然后在继承时明确写: MvcFilter。很多人栽过的坑正是项目里两个命名空间都在自己没注意继承了 Web API 的那个 Filter结果注册后完全不生效控制器里的 ViewBag 反复拿不到数据调试一整天。判断当前项目到底用哪个 Filter 还有一个土办法看报错信息或看类文件的using列表。MVC 5 项目里写视图、Razor、Controller 用System.Web.Mvc如果写的是 ApiController、HttpResponseMessage用的是System.Web.Http。6. 从 .NET Framework 到 ASP.NET CoreViewBag 的变与不变6.1 为什么 Core 里还在用 ViewBag很多从 Framework 时代过来的开发者迁移到 ASP.NET Core 时第一反应是ViewBag 是不是没了。事实是ViewBag 在 ASP.NET Core MVC 里依然存在而且机制几乎一样。它仍然是包装 ViewData 字典的动态对象Controller 基类里仍然有ViewBag属性Razor 视图里照样ViewBag.xxx。在 ASP.NET Core 里Controller 的定义是这样的简化示意public abstract class Controller : ControllerBase { public dynamic ViewBag ViewData; public ViewDataDictionary ViewData { get; set; } // ... }所以迁移时绝大多数ViewBag.Title这种用法可以直接平移零改动。区别主要在命名空间System.Web.Mvc变成了Microsoft.AspNetCore.MvcSystem.Web.Mvc.Controller变成Microsoft.AspNetCore.Mvc.ControllerFilter 也统一归到了Microsoft.AspNetCore.Mvc.Filters命名空间下。6.2 迁移时最容易忽略的细节第一个忽略点是 Filter 注册方式彻底变了。Framework 时代在Global.asax的Application_Start里注册全局 FilterCore 时代是在Startup.cs或 .NET 6 的Program.cs里通过依赖注入注册builder.Services.AddControllersWithViews(options { options.Filters.AddLayoutDataFilter(); });第二个忽略点是 Filter 的命名空间统一了。Core 里不再区分Web API 的 Filter和MVC 的 Filter只有一套Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttributeOnActionExecuting签名也变了从ActionExecutingContext变成了Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext。这个变化对迁移其实是好消息——之前两套 API 打架的问题彻底消失定位理解和写法都更简洁。第三个影响比较大的是 TempData 的存储机制。Core 默认把 TempData 存在 Cookie 里Framework 默认是 Session这意味着存储体积受限、序列化方式不同。如果你之前在 Framework 项目里用 TempData 塞大对象迁移到 Core 时要格外注意建议改成只存小体积的标识数据真正的数据用别的途径重建。第四个细节是匿名类型。Framework 时代你把匿名对象塞进 ViewBag 传视图很正常Core 里如果视图和控制器在不同程序集比如视图从外部程序集加载匿名类型访问会有问题因为匿名类型是internal的。所以迁移后我的建议很明确主体数据一律强类型 ViewModelViewBag 只放 string 和基本类型别放匿名对象。6.3 Controller、Razor Pages、Blazor 里的情况这一点值得单独说因为不同技术栈对 ViewBag 的支持并不一样。MVC 的 Controller 里当然有 ViewBag.cshtml 视图里也有。Razor Pages 的情况稍微特殊。.cshtml页面本身RazorPage仍然有 ViewBag 属性所以在页面标记里ViewBag.xxx是能用的。但 Razor Pages 的代码文件 PageModel 里并没有 ViewBag 属性只有ViewData。也就是说在OnGet方法里你不能写ViewBag.Title xxx得写ViewData[Title] xxx。很多从 MVC 切到 Razor Pages 的人在这里第一次栽跟头。Blazor 则是完全另一套逻辑。组件之间没有 Controller 也没有 ViewBag 的概念数据传递靠参数[Parameter]、级联值CascadingValue、依赖注入的服务或 StateContainer。从 MVC 转 Blazor 的同学不要试图找 ViewBag 的等价物直接按组件参数传数据的思路来设计。还有一个跨版本的细节.NET Core 3.1 到 .NET 5/6/7/8ViewBag 的行为保持稳定没有破坏性变更。但有个值得注意的习惯调整——Core 里_ViewImports.cshtml可以统一注入命名空间这让 ViewBag 的全局公共数据更容易管理。你可以把布局页需要的数据在 Filter 里塞好配合_ViewImports.cshtml里的using页面代码能干净很多。7. 最后的务实建议写到这里把 ViewBag 从入门到避坑的大部分内容都覆盖了。最后按照我自己的实操习惯分享几条压箱底的心得。第一给团队定死 ViewBag 的使用边界。我个人推荐公共数据走 ActionFilter ViewBag业务数据走 ViewModel这个组合。它兼顾了开发效率和类型安全是几经调整后最适合中大型项目的方案。如果项目特别小、就两三个页面那随便用 ViewBag 也问题不大但不要一边用一边重构。第二调试 ViewBag 问题时先检查三件事这个 Action 是不是真的被执行了有没有可能走了别的路由或 Redirect视图里属性名和 Controller 里是否完全一致含大小写有没有其他 Filter 或布局页在同一个键名上二次赋值。按这个顺序排查能解决八成以上的 ViewBag 疑难杂症。第三给 ViewBag 加一个兜底工具方法。项目里可以写一个小的扩展方法让视图里取值更安心public static class ViewDataExtensions { public static string StringValue(this ViewDataDictionary viewData, string key, string fallback null) { var value viewData[key] as string; return string.IsNullOrEmpty(value) ? fallback : value; } public static int IntValue(this ViewDataDictionary viewData, string key, int fallback 0) { var value viewData[key]; if (value is int intVal) { return intVal; } return int.TryParse(value?.ToString(), out var parsed) ? parsed : fallback; } }视图里就可以写ViewData.StringValue(SiteName, 某某平台) ViewData.IntValue(PageSize, 20)这套东西虽然简单但能大幅降低 ViewBag 取值时最常见的 null 和类型错误。代码里少一点玄学页面就少一次白屏。用 ViewBag 可以但别让它变成项目里最不可控的那部分。