仓酷云

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 556|回复: 8
打印 上一主题 下一主题

[学习教程] ASP.NET教程之异步义务:利用义务简化异步编程

[复制链接]
柔情似水 该用户已被删除
跳转到指定楼层
楼主
发表于 2015-1-16 22:20:35 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
就安全性而言,Java已经远远低于VB.NET,更无法与安全性著称的C#相比。 异步编程是完成与程序其他部分并发运转的较年夜开支操纵的一组手艺。常呈现异步编程的一个范畴是有图形化UI的程序情况:当开支较年夜的操纵完成时,解冻UI一般是不成承受的。
别的,异步操纵关于必要并发处置多个客户端哀求的服务器使用程序来讲十分主要。
在理论过程当中呈现的异步操纵的典范例子包含向服务器发送哀求并守候呼应、从硬盘读取数据和运转拼写反省等开支较年夜的盘算。
以一个含UI的使用程序为例。
该使用程序可使用WindowsPresentationFoundation(WPF)或Windows窗体构建。
在此类使用程序中,年夜部分代码都在UI线程上实行,由于它为源自UI控件的事务实行事务处置程序。
当用户单击一个按钮时,UI线程将拔取该动静并实行Click事务处置程序。
如今,假定在Click事务处置程序中,使用程序将哀求发送到服务器并守候呼应:
// !!!
Bad code !!!
void Button_Click(object sender, RoutedEventArgs e) { 
 WebClient client = new WebClient(); 
 client.DownloadFile("http://www.microsoft.com", "index.html"); 
}
此代码中存在一个次要成绩:下载网站必要几秒钟或更长工夫。
接上去,挪用Button_Click必要几秒钟才干前往。
这意味着UI线程会被制止多少秒钟且UI会被解冻。
解冻界面会招致用户体验欠安,这类情形几近都是不成承受的。
要使使用程序UI能随时呼应,直到服务器做出呼应,则需包管下载不是UI线程上的同步操纵,这一点很主要。
让我们实验一下办理解冻UI成绩。
一个大概但并不是最好的办理计划是在分歧线程上与服务器通讯,以便UI线程坚持未制止形态。
上面是一个利用线程池线程与服务器通讯的示例:
// Suboptimal code 
void Button_Click(object sender, RoutedEventArgs e) { 
 ThreadPool.QueueUserWorkItem(_ => { 
  WebClient client = new WebClient(); 
  client.DownloadFile( 
   "http://www.microsoft.com", "index.html"); 
 }); 
}
此代码示例办理了初版存在的成绩:如今Button_Click事务不会制止UI线程,但基于线程的办理计划有三个严峻成绩。
让我们进一步懂得一下这些成绩。
成绩1:华侈线程池线程
我方才先容的办理办法利用来自线程池的线程将哀求发送到服务器并守候服务器呼应。
线程池线程将坚持制止形态,直到服务器呼应。
在对WebClient.DownloadFile的挪用完成之前,线程没法前往到线程池中。
因为UI不会解冻,因而制止线程池线程比制止UI线程要好很多,但它的确会华侈线程池的一个线程。
假如使用程序偶然制止线程池线程一段工夫,功能丧失能够疏忽不计。
可是,假如使用程序常常制止,其呼应才能大概会因线程池接受的压力而下降。
线程池将实验经由过程创立更多线程来应对这类情形,但会形成相称年夜的功能开支。
本文中先容的一切其他异步编程形式可办理华侈线程池线程的成绩。
成绩2:前往了局
利用线程举行异步编程的另外一个困难是:从在匡助器线程上实行的操纵前往值将变得略为混乱。
在最后的示例中,DownloadFile办法将下载的网页写进一个当地文件,因而它具有void前往值。
请看成绩的另外一个版本,您但愿将收到的HTML指定到TextBox(名为HtmlTextBox)的Text属性中,而不是将下载的网页写进一个文件。
完成上述历程的一种想固然的毛病办法以下:
// !!!
Broken code !!!
void Button_Click(object sender, RoutedEventArgs e) { 
 ThreadPool.QueueUserWorkItem(_ => { 
  WebClient client = new WebClient(); 
  string html = client.DownloadString( 
   "http://www.microsoft.com", "index.html"); 
  HtmlTextBox.Text = html; 
 }); 
}
成绩在于UI控件HtmlTextBox被线程池线程修正。
这是一个毛病,缘故原由在于只要UI线程才有权修正UI。
出于多种很充实的来由,WPF和Windows窗体中都存在此限定。
要办理此成绩,您能够在UI线程上捕捉同步情况,然后在线程池线程大将动静公布到该情况:
void Button_Click(object sender, RoutedEventArgs e) { 
 SynchronizationContext ctx = SynchronizationContext.Current; 
 ThreadPool.QueueUserWorkItem(_ => { 
  WebClient client = new WebClient(); 
  string html = client.DownloadString( 
   "http://www.microsoft.com"); 
  ctx.Post(state => { 
   HtmlTextBox.Text = (string)state; 
  }, html); 
 }); 
}
熟悉到从匡助器线程前往值的成绩不单单限于含UI的使用程序,这一点十分主要。
一般,从一个线程将值前往给另外一个线程相称庞大,必要利用同步基元。
成绩3:组合异步操纵
显式处置线程也使得组合异步操纵变得坚苦。
比方,要并行下载多个网页,编写同步代码将变得加倍坚苦,并且更简单堕落。
此类完成将保存仍在实行的异步操纵的计数器。
必需以线程平安的体例修正该计数器,好比说利用Interlocked.Decrement。
一旦计数器抵达零,处置下载的代码便会实行。
一切这统统城市招致相称大批的代码简单堕落。
不必说,利用基于线程的形式乃至将更难准确完成更加庞大的复合形式。
基于事务的形式
利用Microsoft.NETFramework举行异步编程的一个罕见形式是基于事务的模子。
事务模子公然一个办法,以便在操纵完成时启动异步操纵并激发一个事务。
事务形式是公然异步操纵的一个常规,但它不是经由过程接口之类的显式商定。
类完成器能够断定遵守形式的忠厚水平。
显现了准确完成基于事务的异步编程形式所公然的办法示例。
基于事务的形式的办法
public class AsyncExample { 
 // Synchronous methods.
public int Method1(string param); 
 public void Method2(double param); 
 
 // Asynchronous methods.
public void Method1Async(string param); 
 public void Method1Async(string param, object userState); 
 public event Method1CompletedEventHandler Method1Completed; 
 
 public void Method2Async(double param); 
 public void Method2Async(double param, object userState); 
 public event Method2CompletedEventHandler Method2Completed; 
 
 public void CancelAsync(object userState); 
 
 public bool IsBusy { get; } 
 
 // Class implementation not shown.
...
}
WebClient是.NETFramework中的一个类,可经由过程基于事务的形式完成异步操纵。
为了供应DownloadString办法的异步变体,WebClient公然了DownloadStringAsync和CancelAsync办法和DownloadStringCompleted事务。
以下代码显现怎样以异步体例完成我们的示例:
void Button_Click(object sender, RoutedEventArgs e) { 
 WebClient client = new WebClient(); 
 client.DownloadStringCompleted += eventArgs => { 
   HtmlTextBox.Text = eventArgs.Result; 
 }; 
 client.DownloadStringAsync("http://www.microsoft.com"); 
}
此完成办理了基于线程的低效办理计划的第1个成绩:不用要的线程制止。
对DownloadStringAsync的挪用会当即前往,而不会制止UI线程或线程池线程。
下载在背景实行,一旦下载完成,DownloadStringCompleted事务将在响应线程上实行。
请注重,DownloadStringCompleted事务处置程序在响应线程上实行,不必要SynchronizationContext代码,而基于线程的办理计划则必要此代码。
在背景,WebClient主动捕捉SynchronizationContext并接着将回调公布到该情况。
完成基于事务的形式的类一般可确保Completed处置程序在响应线程上实行。
基于事务的异步编程形式不会制止没有需要制止的线程,从这个角度讲该形式是高效的,并且它是.NETFramework中普遍利用的两种形式之一。
不外,基于事务的形式有几个限定:
该形式长短正式且仅仅根据常规的,类能够偏离该形式。
将多个异步操纵组合起来大概会相称坚苦,比方处置并行启动的异步操纵或处置异步操纵序列。
您没法轮询和反省异步操纵是不是已完成。
利用这些范例时必需非常当心。
比方,假如利用一个实例处置多个异步操纵,则必需对注册事务处置程序举行编码,以便仅处置一个方针异步操纵,即便屡次挪用该处置程序也是云云。
即便没有需要在UI线程上实行,也将一直在启动异步操纵时捕捉的SynchronizationContext上挪用事务处置程序,从而招致分外的功能开支。
难以优秀完成,而且必要界说多个范例(比方,事务处置程序或事务参数)。
列出了.NETFramework4类的几个示例,这些类完成基于事务的异步形式。
.NET类中基于事务的异步形式示例
类操纵
System.Activities.WorkflowInvokerInvokeAsync
System.ComponentModel.BackgroundWorkerRunWorkerAsync
System.Net.Mail.SmtpClientSendAsync
System.Net.NetworkInformation.PingSendAsync
System.Net.WebClientDownloadStringAsync

IAsyncResult形式
在.NET中完成异步操纵的另外一个常规是IAsyncResult形式。
与基于事务的模子比拟,IAsyncResult是更初级的异步编程办理计划。
在IAsyncResult形式中,利用Begin和End办法公然异步操纵。
能够挪用Begin办法来启动异步操纵,并传进操纵完成时将挪用的托付。
能够从回调挪用End办法,该办法前往异步操纵的了局。
大概,能够轮询操纵是不是已完成大概同步守候该操纵,而不是供应回调。
以Dns.GetHostAddresses办法为例,该办法承受一个主机名并前往该主机名剖析后的IP地点数组。
该办法同步版本的署名以下所示:
public static IPAddress[] GetHostAddresses( 
 string hostNameOrAddress) 
The asynchronous version of the method is exposed as follows: 
public static IAsyncResult BeginGetHostAddresses( 
 string hostNameOrAddress, 
 AsyncCallback requestCallback, 
 Object state) 
 
public static IPAddress[] EndGetHostAddresses( 
 IAsyncResult asyncResult)
以下示例利用BeginGetHostAddresses和EndGetHostAddresses办法异步查询DNS以取得地点www.microsoft.com:
static void Main() { 
 Dns.BeginGetHostAddresses( 
  "www.microsoft.com", 
  result => { 
   IPAddress[] addresses = Dns.EndGetHostAddresses(result); 
   Console.WriteLine(addresses[0]); 
  }, 
  null); 
 Console.ReadKey(); 
}
列出了多少.NET类,这些类利用基于事务的形式完成异步操纵。
经由过程对照和,您将注重到某些类完成基于事务的形式,某些类完成IAsyncResult形式,而某些类完成两种形式。
.NET类中IAsyncResult的示例
类操纵
System.ActionBeginInvoke
System.IO.StreamBeginRead
System.Net.DnsBeginGetHostAddresses
System.Net.HttpWebRequestBeginGetResponse
System.Net.Sockets.SocketBeginSend
System.Text.RegularExpressions.MatchEvaluatorBeginInvoke
System.Data.SqlClient.SqlCommandBeginExecuteReader
System.Web.DefaultHttpHandlerBeginProcessRequest

从汗青角度讲,IAsyncResult形式作为完成异步API的高功能办法被引进.NETFramework1.0。
不外,它与UI线程举行交互必要分外的事情,很难准确完成,并且难以利用。
在.NETFramework2.0中引进基于事务的形式简化了IAsyncResult未能办理的UI方面的成绩,该形式偏重于以下计划:UI使用程序启动单个异步使用程序,然后与其一同运转。
义务形式
.NETFramework4中引进了一个新范例System.Threading.Tasks.Task,作为暗示异步操纵的一种体例。
一个Task可暗示在CPU上实行的一项一般盘算:
static void Main() { 
 Task<double> task = Task.Factory.StartNew(() => { 
  double result = 0; 
  for (int i = 0; i < 10000000; i++) 
   result += Math.Sqrt(i); 
  return result; 
 }); 
 
 Console.WriteLine("The task is running asynchronously..."); 
 task.Wait(); 
 Console.WriteLine("The task computed: {0}", task.Result); 
}
默许情形下,利用StartNew办法创立的Task与在线程池上实行代码的Task绝对应。
可是,Task加倍通用而且可暗示恣意异步操纵,乃至是与服务器绝对应(大概说通讯)或从磁盘读取数据的那些操纵。
TaskCompletionSource是创立暗示异步操纵的Task的惯例机制。
TaskCompletionSource只与一项义务相干联。
一旦对TaskCompletionSource挪用SetResult办法,相干联的Task便会停止,前往Task的了局值(请拜见)。
利用TaskCompletionSource
static void Main() { 
 // Construct a TaskCompletionSource and get its 
 // associated Task 
 TaskCompletionSource<int> tcs = 
  new TaskCompletionSource<int>(); 
 Task<int> task = tcs.Task; 
 
 // Asynchronously, call SetResult on TaskCompletionSource 
 ThreadPool.QueueUserWorkItem( _ => { 
  Thread.Sleep(1000); // Do something 
  tcs.SetResult(123); 
 }); 
 
 Console.WriteLine( 
  "The operation is executing asynchronously..."); 
 task.Wait(); 
 
 // And get the result that was placed into the task by 
 // the TaskCompletionSource 
 Console.WriteLine("The task computed: {0}", task.Result); 
}
在这里,我利用一个线程池线程对TaskCompletionSource挪用SetResult。
不外,要注重的主要一点是,对TaskCompletionSource有会见权限的任何代码都能够挪用SetResult办法,好比Button.Click事务的事务处置程序、完成某些盘算的Task和因服务器呼应某个哀求而激发的事务等。
因而,TaskCompletionSource是完成异步操纵的很惯例的机制。
转换IAsyncResult形式
要利用Task举行异步编程,很主要的一点是可以与利用较旧模子公然的异步操纵举行互操纵。
固然TaskCompletionSource能够封装任何异步操纵并将其作为Task公然,但TaskAPI供应一种便利的机制将IAsyncResult形式转换为Task,即FromAsync办法。
以下示例利用FromAsync办法将基于IAsyncResult的异步操纵Dns.BeginGetHostAddresses转换为Task:
static void Main() { 
 Task<IPAddress[]> task = 
  Task<IPAddress[]>.Factory.FromAsync( 
   Dns.BeginGetHostAddresses, 
   Dns.EndGetHostAddresses, 
   "http://www.microsoft.com", null); 
 ...
}
FromAsync使得将IAsyncResult异步操纵转换为义务十分简单。
实践上,完成FromAsync的体例相似于利用ThreadPool的TaskCompletionSource示例。
上面是完成该办法的复杂近似体例,在本例中间接以GetHostAddresses为方针:
static Task<IPAddress[]> GetHostAddressesAsTask( 
 string hostNameOrAddress) { 
 
 var tcs = new TaskCompletionSource<IPAddress[]>(); 
 Dns.BeginGetHostAddresses(hostNameOrAddress, iar => { 
  try { 
   tcs.SetResult(Dns.EndGetHostAddresses(iar)); } 
  catch(Exception exc) { tcs.SetException(exc); } 
 }, null); 
 return tcs.Task; 
}
转换基于事务的形式
也能够利用TaskCompletionSource类将基于事务的异步操纵转换为Task。
Task类不为这一转换供应内置机制,因为基于事务的异步形式仅仅是一种常规,因而惯例机制是不有用的。
上面先容怎样将基于事务的异步操纵转换为义务。
代码示例显现猎取Uri并前往暗示异步操纵WebClient.DownloadStringAsync的Task的办法:
static Task<string> DownloadStringAsTask(Uri address) { 
 TaskCompletionSource<string> tcs = 
  new TaskCompletionSource<string>(); 
 WebClient client = new WebClient(); 
 client.DownloadStringCompleted += (sender, args) => { 
  if (args.Error != null) tcs.SetException(args.Error); 
  else if (args.Cancelled) tcs.SetCanceled(); 
  else tcs.SetResult(args.Result); 
 }; 
 client.DownloadStringAsync(address); 
 return tcs.Task; 
}
利用这一形式和上节中先容的形式,您能够将任何现有的异步形式(基于事务或基于IAsyncResult)转换为Task。
处置和组合义务
那末,为什么利用Task来暗示异步操纵?
次要缘故原由是Task公然办法以便于处置和组合异步操纵。
与IAsyncResult和基于事务的办法分歧,Task供应保存关于异步操纵、怎样与之连接、怎样检索其了局等的一切相干信息的单个工具。
关于Task,您能够做的一件有效的事变是守候它完成。
能够在一个Task上守候,守候汇合中的一切Task完成,或守候汇合中的恣意Task完成。
static void Main() { 
 Task<int> task1 = new Task<int>(() => ComputeSomething(0)); 
 Task<int> task2 = new Task<int>(() => ComputeSomething(1)); 
 Task<int> task3 = new Task<int>(() => ComputeSomething(2)); 
 
 task1.Wait(); 
 Console.WriteLine("Task 1 is definitely done."); 
 
 Task.WaitAny(task2, task3); 
 Console.WriteLine("Task 2 or task 3 is also done."); 
 
 Task.WaitAll(task1, task2, task3); 
 Console.WriteLine("All tasks are done."); 
}
Task的另外一项有效功效是可以企图持续义务,即在另外一个Task完成后当即实行的Task。
与守候相似,您能够企图持续义务在特定Task完成时运转、在汇合中的一切Task完成时运转大概在汇合中的恣意Task完成时运转。
以下示例创立一项查询DNS以取得地点www.microsoft.com的义务。
该义务完成后,将启动持续义务并将了局输入到把持台:
static void Main() { 
 Task<IPAddress[]> task = 
  Task<IPAddress[]>.Factory.FromAsync( 
   Dns.BeginGetHostAddresses, 
   Dns.EndGetHostAddresses, 
   "www.microsoft.com", null); 
 
 task.ContinueWith(t => Console.WriteLine(t.Result)); 
 Console.ReadKey(); 
}
让我们看一下更多风趣的示例,它们展现了义务作为异步操纵暗示情势的壮大功效。
显现了并交运行两个DNS查找的示例。
当异步操纵暗示为义务时,很简单守候多个操纵完成。
并交运行多个操纵
static void Main() { 
 string[] urls = new[] { "www.microsoft.com", "www.msdn.com" }; 
 Task<IPAddress[]>[] tasks = new Task<IPAddress[]>[urls.Length]; 
 
 for(int i=0; i<urls.Length; i++) { 
  tasks[i] = Task<IPAddress[]>.Factory.FromAsync( 
   Dns.BeginGetHostAddresses, 
   Dns.EndGetHostAddresses, 
   urls[i], null); 
 } 
 
 Task.WaitAll(tasks); 
 
 Console.WriteLine( 
  "microsoft.com resolves to {0} IP addresses.
msdn.com resolves to {1}", 
  tasks[0].Result.Length, 
  tasks[1].Result.Length); 
}
让我们看另外一个组合多项义务的示例,它接纳以下三个步骤:
经由过程异步体例并行下载多个HTML页面
处置HTML页面
从HTML页面聚合信息
显现怎样使用上文所示的DownloadStringAsTask办法完成此类盘算。
这类完成的光鲜明显优点是两个分歧的CountParagraphs办法在分歧线程上实行。
在现在多核盘算机流行的前提下,将开支年夜的盘算事情分离到多个线程的程序将取得功能上风。
异步下载字符串
static void Main() { 
 Task<string> page1Task = DownloadStringAsTask( 
  new Uri("http://www.microsoft.com")); 
 Task<string> page2Task = DownloadStringAsTask( 
  new Uri("http://www.msdn.com")); 
 
 Task<int> count1Task = 
  page1Task.ContinueWith(t => CountParagraphs(t.Result)); 
 Task<int> count2Task = 
  page2Task.ContinueWith(t => CountParagraphs(t.Result)); 
 
 Task.Factory.ContinueWhenAll( 
  new[] { count1Task, count2Task }, 
  tasks => { 
   Console.WriteLine( 
    "<P> tags on microsoft.com: {0}", 
    count1Task.Result); 
   Console.WriteLine( 
    "<P> tags on msdn.com: {0}", 
    count2Task.Result); 
 }); 
     
 Console.ReadKey(); 
}
在同步情况中运转义务
偶然,可以企图将在特定同步情况中运转的持续义务会十分有效。
比方,在含UI的使用程序中,可以企图将在UI线程上实行的持续义务一般十分有效。
使Task与同步情况交互的最复杂办法是创立用于捕捉以后线程情况的TaskScheduler。
要为UI线程创立TaskScheduler,请在UI线程上运转时对TaskScheduler范例挪用FromCurrentSynchronizationContext静态办法。
以下示例异步下载www.microsoft.com网页,然后将下载的HTML指定到WPF文本框的Text属性中:
void Button_Click(object sender, RoutedEventArgs e) { 
 TaskScheduler uiTaskScheduler = 
  TaskScheduler.FromCurrentSynchronizationContext() 
 
 DownloadStringAsTask(new Uri("http://www.microsoft.com")) 
  .ContinueWith( 
    t => { textBox1.Text = t.Result; }, 
    uiTaskScheduler); 
}
Button_Click办法的主体将创建终极更新UI的异步盘算,但Button_Click不会守候盘算完成。
如许,UI线程将不会被制止,可持续更新用户界面并呼应用户操纵。
如前所述,在.NETFramework4之前,一般利用IAsyncResult形式或基于事务的形式公然异步操纵。
有了.NETFramework4,您如今即可利用Task类作为异步操纵的另外一种有效的暗示情势。
当暗示为义务时,异步操纵一般更容易于处置和组合。
有关利用义务举行异步编程的更多示例包括在ParallelExtensionsExtras示例中,可从code.msdn.microsoft.com/ParExtSamples下载取得。
  代码下载地点:http://code.msdn.microsoft.com/ParExtSamples/Release/ProjectReleases.aspx?ReleaseId=4179
它有很多缺点的,有兴趣可以到网上去搜索一下。于是微软有发明了“下一代”C++:C++/CLI语言,这个可以解决在.NETFramework中,托管C++产生的问题。在《程序员》杂志上,lippman和李建中合作连载介绍了C++/CLI语言。
分手快乐 该用户已被删除
沙发
发表于 2015-1-19 09:06:49 | 只看该作者
主流网站开发语言之ASP:ASP是微软(Microsoft)所开发的一种后台脚本语言,它的语法和VisualBASIC类似,可以像SSI(ServerSideInclude)那样把后台脚本代码内嵌到HTML页面中。虽然ASP简单易用,但是它自身存在着许多缺陷,最重要的就是安全性问题。
莫相离 该用户已被删除
板凳
发表于 2015-1-25 19:26:03 | 只看该作者
现在主流的网站开发语言无外乎asp、php、asp.net、jsp等。
精灵巫婆 该用户已被删除
地板
发表于 2015-2-3 17:05:46 | 只看该作者
Servlet的形式和前面讲的CGI差不多,它是HTML代码和后台程序分开的。它们的启动原理也差不多,都是服务器接到客户端的请求后,进行应答。不同的是,CGI对每个客户请求都打开一个进程(Process)。
admin 该用户已被删除
5#
发表于 2015-2-9 04:13:29 | 只看该作者
主流网站开发语言之JSP:JSP和Servlet要放在一起讲,是因为它们都是Sun公司的J2EE(Java2platformEnterpriseEdition)应用体系中的一部分。
老尸 该用户已被删除
6#
发表于 2015-2-26 21:35:38 | 只看该作者
逐步缩小出错代码段的范围,最终确定错误代码的位置。
乐观 该用户已被删除
7#
发表于 2015-3-8 18:07:09 | 只看该作者
我觉得什么语言,精通就好,你要做的就是比其他80%的人都厉害,你就能得到只有20%的人才能得到的高薪。
愤怒的大鸟 该用户已被删除
8#
发表于 2015-3-16 10:02:22 | 只看该作者
现在的ASP.net分为两个版本:1.1和2.0Asp.net1.1用VS2003(visualstudio2003)编程。Asp.net2.0用VS2005(visualstudio2005)编程。现在一般开发用的是VS2003。
9#
发表于 2015-3-22 22:11:33 | 只看该作者
ASP在执行的时候,是由IIS调用程序引擎,解释执行嵌在HTML中的ASP代码,最终将结果和原来的HTML一同送往客户端。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|仓酷云 鄂ICP备14007578号-2

GMT+8, 2024-12-23 16:28

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表